简体   繁体   中英

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

Let's say I have these two overloads:

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

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

Can I then in the first function add some compile-time verifiction that the passed argument is a string literal?

EDIT: A clarification on why this would be good in my case; my current high-frequency logging uses only string literals and can hence be optimized a lot when there are non-heap allocation guarantees. The second overload doesn't exist today, but I might want to add it, but then I want to keep the first one for extreme scenarios. :)

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)

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.

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.

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).

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).

Here's a quick example I just whipped up using the printf hack I suggested in the comments above:

#include <cstdio>

#define LOG_MACRO(x) do { if (0) printf(x); Log(x); } while (0)

void Log(const char *message)
{
    // do something
}

void function(void)
{
    const char *s = "foo";
    LOG_MACRO(s);
    LOG_MACRO("bar");
}

Output from compiling this one with Clang appears to be exactly what you're looking for:

$ clang++ -c -o example.o example.cpp
example.cpp:13:15: warning: format string is not a string literal
      (potentially insecure) [-Wformat-security]
    LOG_MACRO(s);
              ^
example.cpp:3:41: note: expanded from macro 'LOG_MACRO'
#define LOG_MACRO(x) do { if (0) printf(x); Log(x); } while (0)
                                        ^
1 warning generated.

I did have to switch to printf rather than wprintf , since the latter appears not to generate the warning - I guess that's probably a Clang bug, though.

GCC's output is similar:

$ g++ -c -o example.o example.cpp
example.cpp: In function ‘void function()’:
example.cpp:13: warning: format not a string literal and no format arguments
example.cpp:13: warning: format not a string literal and no format arguments

Edit: You can see the Clang bug here . I just added a comment about -Wformat-security .

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/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM