实现一个C++的static_warning

C++11标准之后里有static_assert关键字可以做编译期静态断言,可以用来手动扔出一个编译错误,这在写模板、各种底层库的时候可以非常方便地做各种校验,提前把开发的错误用法扼杀住。但是C++标准并没有提供一个能手动扔出编译告警的方法,最近我在写一个库的时候,正好有这么一个需求,想在断言失败的情况下提醒使用该库的开发者(具体是想断言atomic<特定的数据类型>是否为lock-free的,如果不为lock-free的,提醒使用库的开发者可能会有性能问题),这种warning不影响最终的构建生成,但是需要达到提醒使用者的目的,而且最好是static_warning(编译期的),不要影响最终的产物的执行效率。

于是,我翻遍了google,发现在stackoverflow上也有人提出了类似的问题,也在回答中找到了一个比较靠谱的方法。稍微小改了一下拿来用了。

Talk is cheap, show the code:

#if defined(__GNUC__)
#define DEPRECATE(function, message) function __attribute__((deprecated(message)))
#elif defined(_MSC_VER)
#define DEPRECATE(function, message) __declspec(deprecated(message)) function
#else
#define DEPRECATE(function, message)
#endif

#define MACRO_CATI(l, r) MACRO_CATR(l, r)
#define MACRO_CATR(l, r) l##r

namespace static_warning_space
{
    struct pass_type {};
    struct warn_type {};

    template <bool condition>
    struct converter : public pass_type {};

    template <>
    struct converter<false> : public warn_type {};
}

#define STATIC_WARNING(condition, message)                                   \
struct MACRO_CATI(static_warning_, __LINE__) {                               \
    MACRO_CATI(static_warning_, __LINE__)()                                  \
    {                                                                        \
        _(::static_warning_space::converter<(condition)>());                 \
    }                                                                        \
    DEPRECATE(void _(::static_warning_space::warn_type const&), message) {}; \
    void _(::static_warning_space::pass_type const&) {};                     \
}

#define static_warning STATIC_WARNING

下面分段来解析一下实现原理。

C++没有手动触发编译告警的方式,我们得绕个圈来实现,这里是借用了deprecated,将函数声明为弃用,如果使用了弃用的函数,这将触发编译器扔出一个使用已废弃的函数的告警。而事实上,我们会在使用时在message中给出手动扔出告警的真正原因。

#if defined(__GNUC__)
#define DEPRECATE(function, message) function __attribute__((deprecated(message)))
#elif defined(_MSC_VER)
#define DEPRECATE(function, message) __declspec(deprecated(message)) function
#else
#define DEPRECATE(function, message)
#endif

接着往下,这一行的宏用于l和r的拼接,但是为啥定义了两层呢?我也很好奇为啥stackoverflow上这么搞了两层,于是我尝试把它弄成一层,直接使用#define MACRO_CATI(l, r) l##r,发现下文中的__LINE__居然不会被替换成相应的行数了,我去?最后还是没有弄明白是什么原因,等以后搞清楚了再来填坑吧。

#define MACRO_CATI(l, r) MACRO_CATR(l, r)
#define MACRO_CATR(l, r) l##r

后面的是关键内容了,定义一个converter模板结构体并特化false的场景,接着定义static_warning宏为一个结构体,在这个结构体的构造函数里根据condition调用对应的名为_(下划线)的函数。而false的场景里的_函数被指定为DEPRECATE,当condition为false时,构造函数里要执行的即是一个deprecated函数,这就迫使编译器报出warning,message里填上触发static_warning的真实原因,让编译器显示出来。

namespace static_warning_space
{
    struct pass_type {};
    struct warn_type {};

    template <bool condition>
    struct converter : public pass_type {};

    template <>
    struct converter<false> : public warn_type {};
}

#define STATIC_WARNING(condition, message)                                   \
struct MACRO_CATI(static_warning_, __LINE__) {                               \
    MACRO_CATI(static_warning_, __LINE__)()                                  \
    {                                                                        \
        _(::static_warning_space::converter<(condition)>());                 \
    }                                                                        \
    DEPRECATE(void _(::static_warning_space::warn_type const&), message) {}; \
    void _(::static_warning_space::pass_type const&) {};                     \
}

#define static_warning STATIC_WARNING

使用上,直接用static_warning宏即可。

int main()
{
    // ...
    static_warning(false, "Warning in main");
    // ...
}

验证结果,发现g++、clang都正常报warning了,vs居然报了error?!

1>------ 已启动全部重新生成: 项目: Test, 配置: Debug Win32 ------
1>test.cpp
1>d:\test\test.cpp(10): error C4996: 'main::static_warning_10::_': Warning in main
1>d:\test\test.cpp(10): note: 参见“main::static_warning_10::_”的声明
1>已完成生成项目“Test.vcxproj”的操作 - 失败。
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========

原因是这样的,在vs的msvc中,deprecated对应的是编译规则C4996,对于vs2017及以上的版本,msvc编译工具将这条规则视为了“错误”。但是这个“错误”并不会影响最终构建出产物,只是这条规则的告警等级变严格了(类比一下-Werror下的把警告视为错误),因此我们手动改一下这条编译规则的告警等级,编译选项中附加上/w14996,该编译选项让4996编译规则的警告等级为1,即当项目工程的警告等级等于或大于/W1时,会产生该规则的告警,默认的vs工程(包括cmake生成的)的警告等级/W3,这样就能显示出该warning又构建成功了。
【PS: 秉承着挖到底的精神,/w[x]4996,[x]可以取值1~4,[x]为0时该编译选项不存在。】

1>------ 已启动全部重新生成: 项目: Test, 配置: Debug Win32 ------
1>test.cpp
1>d:\test\test.cpp(10): warning C4996: 'main::static_warning_10::_': Warning in main
1>d:\test\test.cpp(10): note: 参见“main::static_warning_10::_”的声明
1>Test.vcxproj -> D:\Test\Debug\Test.exe
1>已完成生成项目“Test.vcxproj”的操作。
========== 全部重新生成: 成功 1 个,失败 0 个,跳过 0 个 ==========

另外,对于在模板中使用static_warning会有问题,由于模板会在编译期去分析开发者是否在代码中实例化模板对应的类/结构体/函数,因此static_warning直接放在类/结构体中将不会报出任何的warning,因为编译器发现没有使用到它们,于是它们压根就不会参与到最后的编译过程,而对于模板函数中的static_warning,msvc也只能报出最后一种实例化情形下的编译告警。这个问题目前还没有好的解决方案。

该问题的示例:

// static_warning用于模板中
template <typename>
class TestClass {
public:
    static_warning(false, "Warning in template");

    void func()
    {
        static_warning(false, "Warning in template(func)");
    }
};

int main()
{
    TestClass<int> testClassInt;
    TestClass<long> testClassLong;
    // 如果没有以下这两处调用,"Warning in template(func)"也不会显示,模板中没有调用的使用的地方会被编译器识别并不将它们参与编译
    testClassInt.func();
    testClassLong.func();
}

编译输出:

1>------ 已启动生成: 项目: LauncherTest, 配置: Debug Win32 ------
1>test.cpp
1>d:\test\test.cpp(12): warning C4996: 'TestClass<long>::func::static_warning_12::_': Warning in template
1>d:\test\test.cpp(12): note: 参见“TestClass<long>::func::static_warning_12::_”的声明
1>d:\test\test.cpp(11): note: 编译 类 模板 成员函数 "void TestClass<long>::func(void)" 时
1>d:\test\test.cpp(39): note: 参见对正在编译的函数 模板 实例化“void TestClass<long>::func(void)”的引用
1>d:\test\test.cpp(36): note: 参见对正在编译的 类 模板 实例化 "TestClass<long>" 的引用
1>Test.vcxproj -> D:\Test\Debug\LauncherTest.exe
1>已完成生成项目“Test.vcxproj”的操作。
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========

只报出了最后实例化的TestClass<long>,而TestClass<int>没有报出warning。


参考:

stackoverflow上别人提出的类似问题:
https://stackoverflow.com/questions/8936063/does-there-exist-a-static-warning

vs开发者论坛中别人提出的C4996在vs2017之后被编译器视为error的问题及相应的解决方案:
https://developercommunity.visualstudio.com/t/c4996-shown-as-error-by-default/164550

附录A:

boost库在1.40之前也提供过static_warning,实现思路也是类似的,但是在1.40及之后版本中就不再有这个文件了,可以参考学习:
https://www.boost.org/doc/libs/1_39_0/boost/static_warning.hpp

附录B:

我们在前面在定义DEPRECATE宏的时候,判断了__GNUC__编译器默认自带的宏,这个宏并不专门表示gcc或g++,所有支持GNU-C扩展的编译器都对这个宏进行定义,包括clang和ICC,参见:
https://stackoverflow.com/questions/38499462/how-to-tell-clang-to-stop-pretending-to-be-other-compilers


本文为原创内容,遵循CC BY-ND 4.0协议,署名-禁止演绎。
转载请注明出处:https://tis.ac.cn/blog/kongdeyou/static_warning_in_cpp/

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注