【问题标题】:C compiler asserts - how to implement?C编译器断言-如何实现?
【发布时间】:2010-10-22 21:02:32
【问题描述】:

我想实现一个“断言”,在错误情况下阻止编译,而不是在运行时失败。

我目前有一个这样定义的,效果很好,但会增加二进制文件的大小。

#define MY_COMPILER_ASSERT(EXPRESSION) switch (0) {case 0: case (EXPRESSION):;}

示例代码(编译失败)。

#define DEFINE_A 1
#define DEFINE_B 1
MY_COMPILER_ASSERT(DEFINE_A == DEFINE_B);

我怎样才能实现它以使其不生成任何代码(以最小化生成的二进制文件的大小)?

【问题讨论】:

  • 我真的不认为用纯 C 创建静态断言是可能的,不过我很想知道!
  • 复制几个好的答案:stackoverflow.com/questions/174356/…
  • 由于这个问题比较老:_Static_assert 及其关联的宏 static_assert 从 C11 开始标准化。这现在是语言中内置的。
  • 优化器当然可以扔掉空开关。

标签: c compiler-construction assertions


【解决方案1】:

我发现这是为 GCC 提供的最容易混淆的错误消息。其他所有东西都有一些关于负尺寸或其他令人困惑的东西的后缀:

#define STATIC_ASSERT(expr, msg)   \
typedef char ______Assertion_Failed_____##msg[1];  __unused \
typedef char ______Assertion_Failed_____##msg[(expr)?1:2] __unused

示例用法:

 unsigned char testvar;
 STATIC_ASSERT(sizeof(testvar) >= 8, testvar_is_too_small);

以及 gcc (ARM/GNU C Compiler : 6.3.1) 中的错误消息:

conflicting types for '______Assertion_Failed_____testvar_is_too_small'

【讨论】:

    【解决方案2】:

    正如 Leander 所说,静态断言正在被添加到 C++11 中,现在它们已经添加了。

    static_assert(exp, message)

    例如

    #include "myfile.hpp"
    
    static_assert(sizeof(MyClass) == 16, "MyClass is not 16 bytes!")
    
    void doStuff(MyClass object) { }
    

    查看上面的cppreference page

    【讨论】:

    【解决方案3】:

    纯标准 C 中的编译时断言是可能的,一点点预处理器技巧使其用法看起来与 assert() 的运行时用法一样干净。

    关键技巧是找到一个可以在编译时评估并且可能导致某些值错误的构造。一个答案是数组的声明不能有负大小。使用 typedef 可以防止成功时分配空间,并保留失败时的错误。

    错误消息本身将隐晦地引用负大小的声明(GCC 说“数组 foo 的大小为负”),因此您应该为数组类型选择一个名称,暗示此错误确实是一个断言检查。

    另一个需要处理的问题是,在任何编译单元中只能typedef 特定类型名称一次。因此,宏必须为每个用法安排一个唯一的类型名称来声明。

    我通常的解决方案是要求宏有两个参数。第一个是断言为真的条件,第二个是在幕后声明的类型名称的一部分。 plinth 的答案暗示使用标记粘贴和 __LINE__ 预定义宏来形成唯一名称,可能不需要额外的参数。

    不幸的是,如果断言检查在包含文件中,它仍然可能与第二个包含文件中相同行号或主源文件中该行号的检查发生冲突。我们可以通过使用宏__FILE__ 来解决这个问题,但它被定义为一个字符串常量,并且没有预处理器技巧可以将字符串常量转换回标识符名称的一部分;更不用说合法的文件名可以包含不是标识符合法部分的字符。

    所以,我建议以下代码片段:

    /** A compile time assertion check.
     *
     *  Validate at compile time that the predicate is true without
     *  generating code. This can be used at any point in a source file
     *  where typedef is legal.
     *
     *  On success, compilation proceeds normally.
     *
     *  On failure, attempts to typedef an array type of negative size. The
     *  offending line will look like
     *      typedef assertion_failed_file_h_42[-1]
     *  where file is the content of the second parameter which should
     *  typically be related in some obvious way to the containing file
     *  name, 42 is the line number in the file on which the assertion
     *  appears, and -1 is the result of a calculation based on the
     *  predicate failing.
     *
     *  \param predicate The predicate to test. It must evaluate to
     *  something that can be coerced to a normal C boolean.
     *
     *  \param file A sequence of legal identifier characters that should
     *  uniquely identify the source file in which this condition appears.
     */
    #define CASSERT(predicate, file) _impl_CASSERT_LINE(predicate,__LINE__,file)
    
    #define _impl_PASTE(a,b) a##b
    #define _impl_CASSERT_LINE(predicate, line, file) \
        typedef char _impl_PASTE(assertion_failed_##file##_,line)[2*!!(predicate)-1];
    

    典型的用法可能是这样的:

    #include "CAssert.h"
    ...
    struct foo { 
        ...  /* 76 bytes of members */
    };
    CASSERT(sizeof(struct foo) == 76, demo_c);
    

    在 GCC 中,断言失败如下所示:

    $ gcc -c 演示.c demo.c:32:错误:数组“assertion_failed_demo_c_32”的大小为负 $

    【讨论】:

    • 如果您不关心可移植性,在 GCC 中,__COUNTER__ 可用于提供唯一标识符以粘贴到 typedef 名称上。它是最近添加的 (4.3)
    • 我很惊讶 C 预处理器从来没有像 COUNTER 这样的东西。只要有宏汇编器,宏汇编器就具有类似的构造,更不用说一个宏定义另一个宏的能力,该宏可用于构建可能需要的任何类型的唯一符号。对我来说不幸的是,我的嵌入式系统项目通常卡在 gcc 3.4.5 左右,如果不是一些(大部分)符合 C89 的专有编译器的话。
    • @RBerteig 您好,我想将此代码用于开源项目。你会根据什么许可它?
    • @HannesLandeholm 如果我自己发布它,我通常会申请 MIT 许可证。在这里发布,没有任何进一步的讨论,可以公平地假设用户内容被许可为CC-BY-SA 的一般声明适用。为此,您可以简单地将“CC-BY-SA 3.0 from stackoverflow.com/a/809465/68204”添加到评论块中。就我个人而言,我不喜欢对代码施加“相同方式共享”的限制,但这里的署名和链接总是合适的。
    • 绝对很酷。 glib 对 G_STATIC_ASSERT() 做同样的事情
    【解决方案4】:

    以下COMPILER_VERIFY(exp) 宏运行良好。

    // 合并参数(扩展参数之后) #define GLUE(a,b) __GLUE(a,b) #define __GLUE(a,b) a ## b #define CVERIFY(expr, msg) typedef char GLUE (compiler_verify_, msg) [(expr) ? (+1) : (-1)] #define COMPILER_VERIFY(exp) CVERIFY (exp, __LINE__)

    它适用于 C 和 C++,并且可以在任何允许使用 typedef 的地方使用。如果表达式为真,它会为 1 个字符的数组生成一个 typedef(这是无害的)。如果表达式为假,它会为 -1 个字符的数组生成一个 typedef,这通常会导致错误消息。作为参数给出的表达式可以是计算为编译时常量的任何内容(因此涉及 sizeof() 的表达式可以正常工作)。这使得它比

    灵活得多 #if (表达式) #错误 #万一

    您仅限于可以由预处理器计算的表达式。

    【讨论】:

    • 我喜欢这个,但它会导致未使用的声明变量发出警告:main.c:22:47: 警告:typedef ‘compiler_verify_39’ 本地定义但未使用 [-Wunused-local-typedefs]。由于我的项目将警告视为错误,因此我无法使用它。
    • 自从我发布这个答案以来的十年里,事情已经发生了变化。如果您的编译器支持 C11,您可以使用现在是该语言一部分的 _Static_assert。
    • @Alex,这个警告可以用(void)(sizeof(TYPEDEF_NAME)); 消除。您可能需要额外的取消引用层来将 GLUE 作为参数传递给 CVERIFY() 宏,以便您可以使用 TYPEDEF_NAME。而(void)ENTITY; 是消除“未使用”警告的一般方式。
    【解决方案5】:

    我知道您对 C 感兴趣,但请查看 boost 的 C++ static_assert。 (顺便说一句,这很可能在 C++1x 中可用。)

    我们也为 C++ 做了类似的事情:

    #define COMPILER_ASSERT(expr) enum { ARG_JOIN(CompilerAssertAtLine, __LINE__) = sizeof( char[(expr) ? +1 : -1] ) }

    显然,这只适用于 C++。 This article 讨论了一种修改它以在 C 中使用的方法。

    【讨论】:

      【解决方案6】:

      嗯,你可以使用static asserts in the boost library

      我相信他们在那里所做的是定义一个数组。

       #define MY_COMPILER_ASSERT(EXPRESSION) char x[(EXPRESSION)];
      

      如果 EXPRESSION 为真,则定义char x[1];,这没问题。如果为 false,则定义 char x[0]; 是非法的。

      【讨论】:

      • 但在 C89 中,无论如何都会中断编译,因为变量只能在作用域的顶部声明。也许将它包裹在大括号中可能会解决它?此外,我认为仅在 ISO C 中禁止使用零大小的数组。如果不在其自己的范围内,您还会收到关于多个声明和未使用变量的大量警告。
      • #define MY_COMPILER_ASSERT(EXPRESSION) do {char x[(EXPRESSION)?1:-1];} while (0) 会更好:在大括号内,因此声明是合法且有范围的,大小 1 和 -1 在所有 C 版本上显然是有效/无效的,并强制您使用尾随 MY_COMPILER_ASSERT(...);分号,用于与 C 中所有其他类似函数的东西在视觉上保持一致。
      • 声明 typedef 优于声明未使用的变量。未使用的 typedef 是无害的,但未使用的变量本身可能会在许多编译器上生成警告。
      【解决方案7】:

      我能找到的关于 C 中静态断言的最佳文章是 pixelbeat。请注意,静态断言被添加到 C++ 0X 中,并且可能会添加到 C1X 中,但这不会持续一段时间。我不知道我提供的链接中的宏是否会增加二进制文件的大小。我怀疑他们不会,至少如果您以合理的优化水平进行编译,但您的里程可能会有所不同。

      【讨论】:

        【解决方案8】:

        使用“#error”是一个有效的预处理器定义,它会导致编译在大多数编译器上停止。例如,您可以这样做,以防止在调试中编译:

        
        #ifdef DEBUG
        #error Please don't compile now
        #endif
        

        【讨论】:

        • 不幸的是,这在预处理器级别中止,所以它无法处理像assert(sizeof(long) == sizeof(void *)) 这样的事情。
        【解决方案9】:

        如果您的编译器设置了 DEBUG 或 NDEBUG 之类的预处理器宏,您可以制作类似的内容(否则您可以在 Makefile 中进行设置):

        #ifdef DEBUG
        #define MY_COMPILER_ASSERT(EXPRESSION)   switch (0) {case 0: case (EXPRESSION):;}
        #else
        #define MY_COMPILER_ASSERT(EXPRESSION)
        #endif
        

        然后,您的编译器只断言调试版本。

        【讨论】:

          【解决方案10】:

          当您编译最终的二进制文件时,将 MY_COMPILER_ASSERT 定义为空白,以便其输出不包含在结果中。仅按照您的调试方式定义它。

          但实际上,您无法以这种方式捕获所有断言。有些只是在编译时没有意义(比如断言一个值不为空)。您所能做的就是验证其他#defines 的值。我不太确定你为什么要这样做。

          【讨论】:

          • 它很有用,因为任何可以在编译时验证的东西在运行时都不需要测试用例。在构建可移植协议实现时,验证有关结构大小和布局的假设很有用。由于大小和偏移量在编译时是已知的,因此首选测试它们。此外,在没有代码生成的情况下实现编译时断言意味着没有理由将其从发布版本中删除。在最坏的情况下,它会用孤立类型名称混淆符号表。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-10-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多