Can a compilation error be forced if a string argument is not a string literal?

前端 未结 7 1865
说谎
说谎 2020-12-17 20:24

Let\'s say I have these two overloads:

void Log(const wchar_t* message)
{
    // Do something
}

void Log(const std::wstring& message)
{
    // Do someth         


        
相关标签:
7条回答
  • 2020-12-17 20:31

    I don't think you can enforce to pass only a string literal to a function, but literals are character arrays, what you can enforce:

    #include <iostream>
    
    template<typename T>
    void log(T) = delete; //Disable everything
    
    template <std::size_t Size>
    void log(const wchar_t (&message)[Size]) //... but const wchar_t arrays
    {
        std::cout << "yay" << std::endl;
    }
    
    const wchar_t * get_str() { return L"meow"; }
    
    int main() {
        log(L"foo"); //OK
    
        wchar_t arr[] = { 'b', 'a', 'r', '0' };
        log(arr); //Meh..
    
    //    log(get_str()); //compile error
    }
    

    Downside is that if you have a runtime character array, it will work as well, but won't work for the usual runtime c-style strings.

    But, if you can work with a slightly different syntax, then the answer is YES:

    #include <cstddef>
    #include <iostream>
    
    void operator"" _log ( const wchar_t* str, size_t size ) {
      std::cout << "yay" << std::endl;
    }
    
    int main() {
      L"Message"_log;
    }
    

    Of course, both solution needs a C++11-compatible compiler (example tested with G++ 4.7.3).

    0 讨论(0)
  • 2020-12-17 20:37

    Adding this alternative for future reference. It comes from the SO question Is it possible to overload a function that can tell a fixed array from a pointer?

    #include <iostream>
    #include <type_traits>
    
    template<typename T>
    std::enable_if_t<std::is_pointer<T>::value>
    foo(T)
    {
        std::cout << "pointer\n";
    }
    
    template<typename T, unsigned sz>
    void foo(T(&)[sz])
    {
        std::cout << "array\n";
    }
    
    int main()
    {
      char const* c = nullptr;
      char d[] = "qwerty";
      foo(c);
      foo(d);
      foo("hello");
    }
    

    The above snippet compiles and runs fine on http://webcompiler.cloudapp.net/

    0 讨论(0)
  • 2020-12-17 20:42

    If you define Log as a macro instead, and call separate methods for literal versus std::wstring handling, some variation of the following should work:

    #define Log(x) ((0[#x] == 'L' && 1[#x] == '"') ? LogLiteral(x) : LogString(x))
    
    void
    LogLiteral (const wchar_t *s) {
        //...do something
    }
    
    void
    LogString (const std::wstring& s) {
        //...do something
    }
    

    The trick is that you need opposing definitions of LogLiteral() so that the compilation will pass, but it should never be called.

    inline void LogLiteral (const std::wstring &s) {
        throw std::invalid_argument(__func__);
    }
    

    This code gives you the behavior of an overloaded Log() method, in that you can pass either a string literal or a non-string literal to the Log() macro, and it will end up calling either LogLiteral() or LogString(). This gives compile time verification in that the compiler will not pass anything except what the code recognizes as a string literal to the call to LogLiteral(). At sufficient optimizations, the conditional branch can be removed, since every instance of the check is static (on GCC, it is removed).

    0 讨论(0)
  • 2020-12-17 20:44

    So this grew out of Keith Thompson's answer... As far as I know, you can't restrict string literals to only normal functions, but you can do it to macro functions (through a trick).

    #include <iostream>
    #define LOG(arg) Log(L"" arg)
    
    void Log(const wchar_t *message) {
        std::wcout << "Log: " << message << "\n";
    }
    
    int main() {
        const wchar_t *s = L"Not this message";
        LOG(L"hello world");  // works
        LOG(s);               // terrible looking compiler error
    }
    

    Basically, a compiler will convert "abc" "def" to look exactly like "abcdef". And likewise, it will convert "" "abc" to "abc". You can use this to your benefit in this case.


    I also saw this comment on the C++ Lounge, and that gave me another idea of how to do this, which gives a cleaner error message:

    #define LOG(arg) do { static_assert(true, arg); Log(arg); } while (false)
    

    Here, we use the fact that static_assert requires a string literal as it's second argument. The error that we get if we pass a variable instead is quite nice as well:

    foo.cc:12:9: error: expected string literal
        LOG(s);
            ^
    foo.cc:3:43: note: expanded from macro 'LOG'
    #define LOG(arg) do { static_assert(true, arg); Log(arg); } while (false)
    
    0 讨论(0)
  • 2020-12-17 20:47

    You can't detect string literals directly but you can detect if the argument is an array of characters which is pretty close. However, you can't do it from the inside, you need to do it from the outside:

    template <std::size_t Size>
    void Log(wchar_t const (&message)[Size]) {
        // the message is probably a string literal
        Log(static_cast<wchar_t const*>(message);
    }
    

    The above function will take care of wide string literals and arrays of wide characters:

    Log(L"literal as demanded");
    wchar_t non_literal[] = { "this is not a literal" };
    Log(non_literal); // will still call the array version
    

    Note that the information about the string being a literal isn't as useful as one might hope for. I frequently think that the information could be used to avoid computing the string length but, unfortunately, string literals can still embed null characters which messes up static deduction of the string length.

    0 讨论(0)
  • 2020-12-17 20:53

    I believe the answer to your question is no -- but here's a way to do something similar.

    Define a macro, and use the # "stringification" operator to guarantee that only a string literal will be passed to the function (unless somebody bypasses the macro and calls the function directly). For example:

    #include <iostream>
    
    #define LOG(arg) Log(#arg)
    
    void Log(const char *message) {
        std::cout << "Log: " << message << "\n";
    }
    
    int main() {
        const char *s = "Not this message";
        LOG("hello world");
        LOG(hello world);
        LOG(s);
    }
    

    The output is:

    Log: "hello world"
    Log: hello world
    Log: s
    

    The attempt to pass s to LOG() did not trigger a compile-time diagnostic, but it didn't pass that pointer to the Log function.

    There are at least two disadvantages to this approach.

    One is that it's easily bypassed; you may be able to avoid that by searching the source code for references to the actual function name.

    The other is that stringifying a string literal doesn't just give you the same string literal; the stringified version of "hello, world" is "\"hello, world\"". I suppose your Log function could strip out any " characters in the passed string. You may also want to handle backslash escapes; for example, "\n" (a 1-character string containing a newline) is stringified as "\\n" (a 2-character string containing a backslash and the letter n).

    But I think a better approach is not to rely on the compiler to diagnose calls with arguments other than string literals. Just use some other tool to scan the source code for calls to your Log function and report any calls where the first argument isn't a string literal. If you can enforce a particular layout for the calls (for example, the tokens Log, (, and a string literal on the same line), that shouldn't be too difficult.

    0 讨论(0)
提交回复
热议问题