如果字符串参数不是字符串文字,是否可以强制编译错误?

时间:2013-09-01 22:30:43

标签: c++ templates

假设我有这两个重载:

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

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

我可以在第一个函数中添加一些编译时验证,传递的参数是字符串文字吗?

编辑:澄清为什么这对我来说是好的;我当前的高频日志记录使用字符串文字,因此在存在非堆分配保证时可以进行大量优化。第二次重载今天不存在,但我可能想添加它,但后来我想保留第一个用于极端场景。 :)

7 个答案:

答案 0 :(得分:12)

所以这是从Keith Thompson's answer开始的...据我所知,你不能将字符串文字限制为只有普通函数,但你可以用宏函数(通过技巧)。

#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
}

基本上,编译器会将"abc" "def"转换为与"abcdef"完全相同。同样,它会将"" "abc"转换为"abc"。在这种情况下,您可以使用此功能。


我还在C ++ Lounge上看到了this comment,这让我想到了如何做到这一点,这给出了一个更清晰的错误信息:

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

这里,我们使用static_assert需要字符串文字作为第二个参数的事实。如果我们传递一个变量,我们得到的错误也很好:

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)

答案 1 :(得分:8)

我相信你问题的答案是否定的 - 但这是一种做类似事情的方法。

定义一个宏,并使用#“stringification”运算符来保证只将一个字符串文字传递给该函数(除非有人绕过宏并直接调用该函数)。例如:

#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);
}

输出结果为:

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

尝试将s传递给LOG()并未触发编译时诊断,但它没有将该指针传递给Log函数。

这种方法至少有两个缺点。

一个是它很容易被绕过;您可以通过在源代码中搜索对实际函数名称的引用来避免这种情况。

另一个是字符串文字的字符串化不只是给你相同的字符串文字; "hello, world"的字符串版本为"\"hello, world\""。我想你的Log函数可以删除传递的字符串中的任何"个字符。您可能还想处理反斜杠转义;例如,"\n"(包含换行符的1个字符的字符串)被字符串化为"\\n"(包含反斜杠和字母n的2个字符的字符串)。

但我认为更好的方法是不要依赖编译器来诊断带有字符串文字之外的参数的调用。只需使用其他工具扫描源代码以调用Log函数,并报告第一个参数不是字符串文字的任何调用。如果您可以为调用强制执行特定布局(例如,令牌Log(和同一行上的字符串文字),那应该不会太困难。

答案 2 :(得分:5)

您无法直接检测字符串文字,但可以检测参数是否是非常接近的字符数组。但是,你不能从内部做到,你需要从外面做:

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);
}

上面的函数将处理宽字符串文字和宽字符数组:

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

请注意,有关字符串作为文字的信息并不像人们希望的那样有用。我经常认为可以使用这些信息来避免计算字符串长度,但不幸的是,字符串文字仍然可以嵌入空字符,这会扰乱字符串长度的静态演绎。

答案 3 :(得分:3)

如果您将Log定义为宏,并为文字与std::wstring处理调用单独的方法,则以下内容的某些变体应该有效:

#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
}

诀窍是你需要反对LogLiteral()的定义,以便编译通过,但永远不应该调用。

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

此代码为您提供了重载Log()方法的行为,因为您可以将字符串文字或非字符串文字传递给Log()宏,它最终会调用LogLiteral()LogString()。这给出了编译时验证,因为除了代码识别为LogLiteral()的调用的字符串文字之外,编译器不会传递任何内容。在充分优化的情况下,可以删除条件分支,因为每个检查实例都是静态的(在GCC上,它被删除)。

答案 4 :(得分:2)

我认为您不能强制只将字符串 literal 传递给函数,但文字是字符数组,您可以强制执行:

#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
}

缺点是如果你有一个运行时字符数组,它也可以工作,但不适用于通常的运行时c风格的字符串。

但是,如果您可以使用稍微不同的语法,那么答案是肯定的:

#include <cstddef>
#include <iostream>

void operator"" _log ( const wchar_t* str, size_t size ) {
  std::cout << "yay" << std::endl;
}

int main() {
  L"Message"_log;
}

当然,这两个解决方案都需要兼容C ++ 11的编译器(使用G ++ 4.7.3进行测试的例子)。

答案 5 :(得分:2)

这是一个简单的例子,我刚刚使用上面评论中建议的printf黑客攻击:

#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");
}

使用Clang编译此输出的输出似乎正是您正在寻找的内容:

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

我确实必须切换到printf而不是wprintf,因为后者似乎没有生成警告 - 我猜这可能是一个Clang错误。

GCC的输出类似:

$ 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

编辑:您可以看到Clang错误here。我刚刚添加了关于-Wformat-security的评论。

答案 6 :(得分:0)

添加此替代方案以供将来参考。它来自SO问题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");
}

以上代码段在http://webcompiler.cloudapp.net/

上编译并运行正常