C语言中的静态断言

129

如何在C语言中(而非C++)实现编译时静态断言,尤其是在GCC编译器下的实现?


对于GCC/Clang上的C11,使用int32_ts进行相等性检查,如果失败,甚至可以让编译器打印出错误值!https://stackoverflow.com/q/53310844/1495449 - Jetski S-type
16个回答

140

C11标准添加了_Static_assert关键字。

这是自gcc-4.6以来实现的

_Static_assert (0, "assert1"); /* { dg-error "static assertion failed: \"assert1\"" } */

第一个占位符需要是一个整型常量表达式。第二个占位符是一个常量字符串字面量,可以很长(_Static_assert(0, L"assertion of doom!"))。

需要注意的是最近版本的clang也实现了这一功能。


14
你可以更加有信心地说,"_Static_assert" 是 C11 标准的一部分,任何支持 C11 的编译器都将具备该功能。这似乎被gcc和clang所实现。 - P.P
2
这个能在文件作用域(任何函数之外)使用吗?因为我在这一行得到了 error: expected declaration specifiers or '...' before 'sizeof' 的错误:static_assert( sizeof(int) == sizeof(long int), "Error!); (顺便说一下,我正在使用的是 C 而不是 C++)。 - user10607
@user10607 我很惊讶这没有起作用...等等,你缺少了错误字符串末尾的引号。加上它再试一次。在我的gcc-4.9中,这对我有用:_Static_assert(sizeof(int) == sizeof(long int),"错误!"); 在我的机器上出现错误。 - emsr
我在Ubuntu上有gcc 4.8.2。漏掉的引号是一种注释笔误(我在代码中有它)。这是几个头文件包含后的文件的第一行。编译器给了我两个完全相同的错误:error: expected declaration specifiers or '...' before 'sizeof'error: expected declaration specifiers or '...' before string constant(他指的是"Error!"字符串)(还要注意:我正在使用-std=c11进行编译。将声明放在函数内部时,一切正常(预期成功或失败)。) - user10607
2
@user10607 我也必须在命令行上指定-std=gnu11。我真的很惊讶4.8和4.8之间会有差异。我有一个只有一行的源代码。我还使用了C标准_Static_assert而不是类似于C ++的static_assert。您需要#include <assert.h>来获取static_assert宏。 - emsr
显示剩余3条评论

114

这在函数和非函数作用域中都有效(但不适用于结构体和联合体内部)。

#define STATIC_ASSERT(COND,MSG) typedef char static_assertion_##MSG[(COND)?1:-1]

STATIC_ASSERT(1,this_should_be_true); 

int main()
{
 STATIC_ASSERT(1,this_should_be_true); 
}
  1. 如果编译时断言无法匹配,则GCC将生成一个几乎可以理解的错误信息:sas.c:4: error: size of array 'static_assertion_this_should_be_true' is negative

  2. 宏应该被更改为为typedef生成唯一的名称(即在static_assert_...名称的结尾连接__LINE__

  3. 可以使用这个代替三元运算符:#define STATIC_ASSERT(COND,MSG) typedef char static_assertion_##MSG[2*(!!(COND))-1],它甚至可以在陈旧的cc65(针对6502 CPU)编译器上工作。

更新:为了完整起见,以下是带有__LINE__的版本

#define STATIC_ASSERT(COND,MSG) typedef char static_assertion_##MSG[(!!(COND))*2-1]
// token pasting madness:
#define COMPILE_TIME_ASSERT3(X,L) STATIC_ASSERT(X,static_assertion_at_line_##L)
#define COMPILE_TIME_ASSERT2(X,L) COMPILE_TIME_ASSERT3(X,L)
#define COMPILE_TIME_ASSERT(X)    COMPILE_TIME_ASSERT2(X,__LINE__)

COMPILE_TIME_ASSERT(sizeof(long)==8); 
int main()
{
    COMPILE_TIME_ASSERT(sizeof(int)==4); 
}

更新2: GCC 特定代码

GCC 4.3(我猜)引入了“error”和“warning”函数属性。如果带有该属性的函数调用不能通过死代码消除(或其他措施)而被消除,则会生成错误或警告。这可用于使用用户定义的失败描述进行编译时断言。现在需要确定如何在命名空间范围内使用它们,而不必借助虚拟函数:

#define CTC(X) ({ extern int __attribute__((error("assertion failure: '" #X "' not true"))) compile_time_check(); ((X)?0:compile_time_check()),0; })

// never to be called.    
static void my_constraints()
{
CTC(sizeof(long)==8); 
CTC(sizeof(int)==4); 
}

int main()
{
}

这是它的外观:

$ gcc-mp-4.5 -m32 sas.c 
sas.c: In function 'myc':
sas.c:7:1: error: call to 'compile_time_check' declared with attribute error: assertion failure: `sizeof(int)==4` not true

2
在Visual Studio中,它只显示“负下标”,没有提到变量名... - szx
北欧主机 - 在您的回答中选项3在clang上不起作用。 - Elazar
1
关于最后一个(GCC 4.3+特定)解决方案:这非常强大,因为它可以检查优化器可以找出的任何内容,但如果未启用优化,则会失败。然而,最低限度的优化级别(-Og)通常足以使其工作,并且不应干扰调试。如果未定义__OPTIMIZE__(和__GNUC__),则可以考虑将静态断言设置为无操作或运行时断言。 - Søren Løvborg
如果参数不是常量,则此操作可以悄悄地成功。例如,如果n是变量,则此操作不会报错:STATIC_ASSERT(n == 42, always_succeeds)。在函数内部,这将定义一个变量数组,这是可以的。但是,如果n是一个编译时常量且与42不同,则此操作将失败。非常令人困惑。 - MarcH
我刚刚进行了大量的重写,展示了我所有的新发现,包括我在之前评论中所说的内容,请点击此处查看。 - Gabriel Staples
显示剩余8条评论

15
这个答案在2022年4月17日得到了极大的改进,作为复活节礼物。我相信这是这里最详尽和完整的答案,因为我提供了3个不同复杂度的解决方案,以涵盖不同版本的C和C++,而且我最后提供的版本涵盖了所有的C和C++版本,这一开始似乎是不可能的。
您可以在我的文件static_assert_for_all_versions_of_c_and_cpp.c中查看和测试适用于所有C和C++版本的代码。
请注意,ISO C90不允许使用C++风格的注释,因此我的代码示例必须只使用C风格的注释(/* */),而不是C++风格的//注释,以便我的代码能够在-std=c90中编译。
要获得STATIC_ASSERT(test_for_true)的快速摘要,请参阅以下内容:
如果你想要一个快速而超级简单的宏,在任何版本的C(使用gcc编译)或C++的任何版本(从C++11或更高版本开始),请参考下一节底部的两个简单宏代码块:“C和C++中可用的静态断言声明总结”。以下是这些宏的复制和粘贴版本,供你方便使用,接下来是一个更复杂但通用的STATIC_ASSERT(test_for_true)宏,适用于任何版本的C或C++。以下是我的三个主要解决方案:
  • [最简单的选择!] STATIC_ASSERT(test_for_true) 仅适用于 C11 或更高版本以及 C++11 或更高版本:

    #include <assert.h>
    #define STATIC_ASSERT(test_for_true) \
        static_assert((test_for_true), "(" #test_for_true ") 失败")
    
  • 或者 [我的首选],STATIC_ASSERT(test_for_true) 适用于 任何 版本的 C(作为 gcc 编译器的扩展),包括 C90、C99、C11、C17 等,以及 C++11 或更高版本(无法处理旧版本的 C++):

    #ifdef __cplusplus
        #ifndef _Static_assert
            #define _Static_assert static_assert
        #endif
    #endif
    #define STATIC_ASSERT(test_for_true) \
        _Static_assert((test_for_true), "(" #test_for_true ") 失败")
    
  • STATIC_ASSERT(test_for_true) 适用于 任何 版本的 C 和 C++:

    如果你想要一个单一的 STATIC_ASSERT 宏在 所有 版本的 C 和 C++ 中都能工作,我在下面的部分中提供了它。你可以在底部的 "测试摘要" 部分中看到我用来测试它的构建命令和语言设置。使静态断言在 pre-C++11 中工作,比如 C++98、C++03 等,是困难的部分!下面的 _Static_assert_hack 宏就是处理早期版本 C++ 的部分。以下是处理所有版本的 C 和 C++ 的完整代码块,为了方便起见,大部分注释已被删除:

    // 参考:https://dev59.com/qXA75IYBdhLWcg3wRWyc#54993033
    
    #define CONCAT_(prefix, suffix) prefix##suffix
    #define CONCAT(prefix, suffix) CONCAT_(prefix, suffix)
    #define MAKE_UNIQUE_VARIABLE_NAME(prefix) CONCAT(prefix##_, __LINE__)
    
    /* 需要用于 **pre-C++11** 的静态断言 hack,如 C++98、C++03 等。
     * - 它只适用于 C++,不适用于 C!
     */
    #define _Static_assert_hack(expression, message) \
        struct MAKE_UNIQUE_VARIABLE_NAME(static_assertion_failed) \
        { \
            _Pragma("GCC diagnostic push") \
            _Pragma("GCC diagnostic ignored \"-Wunused-local-typedefs\"") \
            typedef char static_assertion_failed[(expression) ? 1 : -1]; \
            _Pragma("GCC diagnostic pop") \
        }
    
    /* 仅适用于 C++:
     * 参考:https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html 
     */
    #ifdef __cplusplus
        #if __cplusplus < 201103L
            /* 适用于 pre-C++11 */
            #ifndef _Static_assert
                #define _Static_assert _Static_assert_hack
            #endif
        #else
            /* 适用于 C++11 或更高版本 */
            #ifndef _Static_assert
                #define _Static_assert static_assert
            #endif
        #endif
    #endif
    
    /* 适用于 C **和** C++: */
    #define STATIC_ASSERT(test_for_true) \
        _Static_assert((test_for_true), "(" #test_for_true ") 失败")
    
  • C和C++中可用的静态断言声明的摘要:
    了解以下内容:
    C语言:_Static_assert(expression, message)C11或更高版本中可用。 根据上面的cppreference.com社区wiki链接,static_assert也作为一个方便的宏,以匹配C++11中的命名,在头文件<assert.h>中也可用。因此,要在C11或更高版本中获得类似C++的static_assert作为宏,您还应该#include <assert.h>_Static_assert(expression)(即没有message部分)也可在C23或更高版本中使用。
    C++语言:static_assert(expression, message)C++11或更高版本中可用。 static_assert(expression)(即没有message部分)也可在C++17或更高版本中使用。
    gcc编译器:
    gcc编译器版本4.6及更高版本开始,_Static_assert作为gcc扩展支持所有版本的C,包括c90、c99、c11、c17等。
    而且,根据上述C11标准,static_assert作为宏也可用于C11或更高版本,如果您还#include <assert.h>
    g++编译器版本4.3及更高版本开始,static_assert作为C++11或更高版本的关键字被支持。在C++中,您不需要像在C中那样#include <assert.h>来使用这种格式。
    GCC源码:https://www.gnu.org/software/gnulib/manual/html_node/assert_002eh.html(重点添加):

    更早的平台根本不支持static_assert_Static_assert。例如,4.6之前的GCC版本不支持_Static_assert4.3之前的G++版本不支持static_assert,这是由C11和C++11标准化的。

    C的_Static_assert和C++的static_assert是可以在不包括<assert.h>的情况下使用的关键字。Gnulib的替代方法是需要包括<assert.h>的宏。

    另请参阅:https://gcc.gnu.org/wiki/C11Status - 我从主要答案中获得了这个链接。
    我喜欢编写一个STATIC_ASSERT的包装宏,将参数简化为1个,并自动产生message参数,这样我就可以使用STATIC_ASSERT(expression)而不是STATIC_ASSERT(expression, message)。以下是如何轻松实现这一点的方法:
    只针对C11或更高版本和C++11或更高版本: ```c #include #define STATIC_ASSERT(test_for_true) \ static_assert((test_for_true), "(" #test_for_true ") failed") ```
    或者[我的首选],适用于任何版本的C(作为gcc编译器的扩展),包括C90、C99、C11、C17等,以及C++11或更高版本(无法处理旧版本的C++): ```c #ifdef __cplusplus #ifndef _Static_assert #define _Static_assert static_assert #endif #endif #define STATIC_ASSERT(test_for_true) \ _Static_assert((test_for_true), "(" #test_for_true ") failed") ```
    对于早于C++11的C++版本,您将需要使用一个技巧来获得一个可用的静态断言。使用我下面介绍的复杂的pre-C++11静态断言技巧,或者更好的办法是升级到C++11或更高版本。

    在我的static_assert_for_all_versions_of_c_and_cpp.c中测试上述代码片段。

    非gcc的pre-C11pre-C++11的静态断言技巧

    1/2. 仅适用于C语言(例如:非gcc的pre-C11

    对于gcc的pre-C11,gcc已经定义了_Static_assert(expression, message),这真的很好。所以,只需使用它并完成上述操作!但是,如果你没有使用gcc编译器呢?你能做些什么呢?

    嗯,我注意到了一些非常有趣的事情。如果我在C90中使用_Static_assert(1 > 2, "this should fail");,并使用以下构建命令:

    gcc -Wall -Wextra -Werror -O3 -std=c90 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    

    我对那个失败的 _Static_assert 得到了这个编译时错误。这个错误太奇怪了!虽然这不是一个意外的构建错误,但这是静态断言失败错误,因为他们还在使用一个针对这个版本的 C 的黑客来实现编译时静态断言!
    In file included from /usr/include/features.h:461,
                     from /usr/include/x86_64-linux-gnu/bits/libc-header-start.h:33,
                     from /usr/include/stdint.h:26,
                     from /usr/lib/gcc/x86_64-linux-gnu/9/include/stdint.h:9,
                     from static_assert_for_all_versions_of_c_and_cpp.c:73:
    static_assert_for_all_versions_of_c_and_cpp.c: In function ‘main’:
    static_assert_for_all_versions_of_c_and_cpp.c:224:5: error: negative width in bit-field ‘__error_if_negative’
      224 |     _Static_assert(1 > 2, "this should fail");
          |     ^~~~~~~~~~~~~~
    

    如果我在GitHub上访问gcc源代码镜像(https://github.com/gcc-mirror/gcc),克隆该存储库,然后使用grep或ripgrep搜索__error_if_negative,我只能在一个位置找到结果,即这里:https://github.com/gcc-mirror/gcc/blob/master/libgcc/soft-fp/soft-fp.h#L69-L71
    # define _FP_STATIC_ASSERT(expr, msg)                   \
      extern int (*__Static_assert_function (void))             \
        [!!sizeof (struct { int __error_if_negative: (expr) ? 2 : -1; })]
    

    这是一个静态断言的技巧,你可以在非gcc版本的pre-C11 C中借用和使用!
    只需将_FP_STATIC_ASSERT替换为_Static_assert,就像这样:
    # define _Static_assert(expr, msg)                   \
      extern int (*__Static_assert_function (void))             \
        [!!sizeof (struct { int __error_if_negative: (expr) ? 2 : -1; })]
    

    使用上述的_Static_assert技巧的注意事项:
    它只在C语言中有效,而不在C++中有效! 它在ISO C的结构体或联合体中不起作用,即当您使用-std=c90、-std=c99等选项时。 我相信,如果您使用gnu C语言,例如-std=gnu90或-std=gnu99,它会起作用。 如果您尝试在联合体或结构体中使用它,就像这样: typedef union data_u { data_t data; uint8_t bytes[sizeof(data_t)];
    _Static_assert(2 > 1, "this should pass"); _Static_assert(5 > 4, "this should pass"); } data_union_t; ...那么您将看到关于“expected specifier-qualifier-list before ‘extern’”的超级晦涩的错误。这不是因为静态断言表达式失败(为假),而是因为在这种用法中,静态断言的hack是有问题的。请注意,上面的hack中使用了extern,所以它出现在错误中。
    2/2. 仅适用于C++(例如:对于pre-C++11有用)
    我发现在pre-C++11 C++中实现一个漂亮的静态断言技巧非常棘手,但我成功了!这是一项艺术品,但它似乎工作得很好,而且很稳健。它也像C++11的static_assert一样在结构体和联合体中工作!这就是它。你可以在这里测试它,我的static_assert_for_all_versions_of_c_and_cpp.c
    #define CONCAT_(prefix, suffix) prefix##suffix
    #define CONCAT(prefix, suffix) CONCAT_(prefix, suffix)
    #define MAKE_UNIQUE_VARIABLE_NAME(prefix) CONCAT(prefix##_, __LINE__)
    
    /* Static assert hack required for **pre-C++11**, such as C++98, C++03, etc. */
    /* - It works only with C++, NOT with C! */
    #define _Static_assert_hack(expression, message) \
        struct MAKE_UNIQUE_VARIABLE_NAME(static_assertion_failed) \
        { \
            _Pragma("GCC diagnostic push") \
            _Pragma("GCC diagnostic ignored \"-Wunused-local-typedefs\"") \
            typedef char static_assertion_failed[(expression) ? 1 : -1]; \
            _Pragma("GCC diagnostic pop") \
        }
    

    我的最终版本:一个适用于所有版本的C和所有版本的C++的STATIC_ASSERT(),在使用gcc编译时有效

    只需稍作修改,改变使用的风格和时间,下面的代码也可以在非gcc编译器上适用于任何版本的C和C++。

    按照目前的编写方式,我预计它可以在所有版本的C和gnu C以及所有版本的C++和gnu++上使用,无论是使用gcc/g++编译器还是LLVM clang编译器。

    这是最终版本:一个静态断言来处理任何版本的C或C++!

    /* --------------------------------- START ---------------------------------- */
    /* OR [BEST], for **any version of C OR C++**: */
    
    /* See: https://dev59.com/eHI-5IYBdhLWcg3w9tdw#71899854 */
    #define CONCAT_(prefix, suffix) prefix##suffix
    /* Concatenate `prefix, suffix` into `prefixsuffix` */
    #define CONCAT(prefix, suffix) CONCAT_(prefix, suffix)
    /* Make a unique variable name containing the line number at the end of the */
    /* name. Ex: `uint64_t MAKE_UNIQUE_VARIABLE_NAME(counter) = 0;` would */
    /* produce `uint64_t counter_7 = 0` if the call is on line 7! */
    #define MAKE_UNIQUE_VARIABLE_NAME(prefix) CONCAT(prefix##_, __LINE__)
    
    /* Static assert hack required for **pre-C++11**, such as C++98, C++03, etc. */
    /* - It works only with C++, NOT with C! */
    /* See: */
    /* 1. [my ans with this] https://dev59.com/qXA75IYBdhLWcg3wRWyc#54993033 */
    /* 1. Info. on `_Pragma()`: https://dev59.com/43A75IYBdhLWcg3wSm64#47518775 */
    /* 1. The inspiration for this `typedef char` array hack as a struct  */
    /*    definition: https://dev59.com/qXA75IYBdhLWcg3wRWyc#3385694 */
    /* Discard the `message` portion entirely. */
    #define _Static_assert_hack(expression, message) \
        struct MAKE_UNIQUE_VARIABLE_NAME(static_assertion_failed) \
        { \
            _Pragma("GCC diagnostic push") \
            _Pragma("GCC diagnostic ignored \"-Wunused-local-typedefs\"") \
            typedef char static_assertion_failed[(expression) ? 1 : -1]; \
            _Pragma("GCC diagnostic pop") \
        }
    
    /* For C++ only: */
    /* See: https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html */
    #ifdef __cplusplus
        #if __cplusplus < 201103L
            /* for pre-C++11 */
            #ifndef _Static_assert
                #define _Static_assert _Static_assert_hack
            #endif
        #else
            /* for C++11 or later */
            #ifndef _Static_assert
                #define _Static_assert static_assert
            #endif
        #endif
    #endif
    
    /* For C **and** C++: */
    #define STATIC_ASSERT(test_for_true) \
        _Static_assert((test_for_true), "(" #test_for_true ") failed")
    /* ---------------------------------- END ----------------------------------- */
    

    我用来帮助我构建这个的参考资料在上面源代码的注释中。以下是从那里复制的可点击链接:
    1. [我的回答] 如何使用宏自动生成带有行号的唯一变量名
      1. 我主要是从@Jarod42这里学到的,还有从@Adam.Rosenfield这里学到的。
    2. _Pragma()的信息:如何在几行代码中禁用GCC警告
    3. 这个typedef char数组hack作为结构定义的灵感来源:
      1. @Nordic.Mainframe的回答
      2. gcc源代码静态断言hack
    4. gcc预定义宏:
      1. https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
      2. https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html
    C++03示例静态断言错误输出:
    在上述的STATIC_ASSERT中,使用在C++11之前的情况下,这里有一些样例代码,其中包含一个预期会失败的静态断言,因为它是错误的。
    typedef union data_u
    {
        data_t data;
        uint8_t bytes[sizeof(data_t)];
    
        STATIC_ASSERT(2 > 2); /* this should fail */
    } data_union_t;
    

    以下是构建命令和失败输出的样子。对于C++中的一个失败的静态断言来说,这里有一大堆错误信息,但对于像这样的黑科技来说,这是可以预料的,而之前提到的gcc C90黑科技用于_Static_assert的方式也不好:
    eRCaGuy_hello_world/c$ g++ -Wall -Wextra -Werror -O3 -std=c++03 static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    static_assert_for_all_versions_of_c_and_cpp.c:129:67: error: narrowing conversion of ‘-1’ from ‘int’ to ‘long unsigned int’ is ill-formed in C++11 [-Werror=narrowing]
      129 |         typedef char static_assertion_failed[(expression) ? 1 : -1]; \
          |                                                                   ^
    static_assert_for_all_versions_of_c_and_cpp.c:139:36: note: in expansion of macro ‘_Static_assert_hack’
      139 |             #define _Static_assert _Static_assert_hack
          |                                    ^~~~~~~~~~~~~~~~~~~
    static_assert_for_all_versions_of_c_and_cpp.c:151:5: note: in expansion of macro ‘_Static_assert’
      151 |     _Static_assert((test_for_true), "(" #test_for_true ") failed")
          |     ^~~~~~~~~~~~~~
    static_assert_for_all_versions_of_c_and_cpp.c:187:5: note: in expansion of macro ‘STATIC_ASSERT187 |     STATIC_ASSERT(2 > 2);
          |     ^~~~~~~~~~~~~
    static_assert_for_all_versions_of_c_and_cpp.c:129:59: error: size ‘-1’ of array ‘static_assertion_failed’ is negative
      129 |         typedef char static_assertion_failed[(expression) ? 1 : -1]; \
          |                                              ~~~~~~~~~~~~~^~~~~~~~
    static_assert_for_all_versions_of_c_and_cpp.c:139:36: note: in expansion of macro ‘_Static_assert_hack’
      139 |             #define _Static_assert _Static_assert_hack
          |                                    ^~~~~~~~~~~~~~~~~~~
    static_assert_for_all_versions_of_c_and_cpp.c:151:5: note: in expansion of macro ‘_Static_assert’
      151 |     _Static_assert((test_for_true), "(" #test_for_true ") failed")
          |     ^~~~~~~~~~~~~~
    static_assert_for_all_versions_of_c_and_cpp.c:187:5: note: in expansion of macro ‘STATIC_ASSERT187 |     STATIC_ASSERT(2 > 2);
          |     ^~~~~~~~~~~~~
    cc1plus: all warnings being treated as errors
    
    

    测试摘要

    请参阅static_assert_for_all_versions_of_c_and_cpp.c

    最终的STATIC_ASSERT(test_for_true)宏位于上方,它适用于所有版本的C和C++,在Linux Ubuntu 20.04上使用gcc编译器版本(gcc --version9.4.0gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0)进行了测试。

    这里列出了各种构建命令和语言,经过测试并且可正常工作。再次强调,在所有这些版本中,唯一不允许在结构体和联合体内使用STATIC_ASSERT()宏的是-std=c90-std=c99!其他所有选项都支持在任何地方使用STATIC_ASSERT,包括函数外部、函数内部以及结构体和联合体内部。
    # -------------------
    # 1. In C:
    # -------------------
    
    gcc -Wall -Wextra -Werror -O3 -std=c90 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    gcc -Wall -Wextra -Werror -O3 -std=c99 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    gcc -Wall -Wextra -Werror -O3 -std=c11 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    gcc -Wall -Wextra -Werror -O3 -std=c17 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    
    # gnu C
    gcc -Wall -Wextra -Werror -O3 -std=gnu90 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    gcc -Wall -Wextra -Werror -O3 -std=gnu99 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    gcc -Wall -Wextra -Werror -O3 -std=gnu11 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a
    # [my default C build cmd I use today]:
    gcc -Wall -Wextra -Werror -O3 -std=gnu17 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a -lm && bin/a  
    
    # -------------------
    # 2. In C++
    # -------------------
    
    g++ -Wall -Wextra -Werror -O3 -std=c++98 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    g++ -Wall -Wextra -Werror -O3 -std=c++03 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    g++ -Wall -Wextra -Werror -O3 -std=c++11 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    g++ -Wall -Wextra -Werror -O3 -std=c++17 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    # gnu++
    g++ -Wall -Wextra -Werror -O3 -std=gnu++98 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    g++ -Wall -Wextra -Werror -O3 -std=gnu++03 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    g++ -Wall -Wextra -Werror -O3 -std=gnu++11 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a
    # [my default C++ build cmd I use today]:
    g++ -Wall -Wextra -Werror -O3 -std=gnu++17 \
        static_assert_for_all_versions_of_c_and_cpp.c -o bin/a && bin/a  
    

    相关:

    1. 使用 static_assert 来检查传递给宏的类型 [我的回答]
      1. https://en.cppreference.com/w/cpp/types/is_same
      2. https://en.cppreference.com/w/cpp/language/decltype
    2. 使用 static_assert 来检查传递给宏的类型
    3. 如何在 C 中使用 static_assert 来检查传递给宏的参数类型

    待办事项

    尝试使用BUILD_BUG_ON()替代。在“适用于所有版本的C和C++”情况下,它是否能够取代或更好地工作?

    1
    为什么要这么复杂,当assert.h中有一个static_assert宏时? - Kami Kaze
    1
    static_assert自C11起在C中定义。它是一个宏,展开为_Static_assert。https://en.cppreference.com/w/c/error/static_assert。此外,与您的答案相反,在gcc中c99和c90中不提供`_Static_assert`(仅在gnu99和gnu90中)。这符合标准。基本上,您做了很多额外的工作,只有在使用gnu90和gnu99编译时才带来好处,并且使实际用例微不足道。 - Kami Kaze
    1
    好的,我看到我在某种程度上是错误的。_Static_assert 在gcc的c90/c99中被定义,但“问题”是它与c11的工作方式不同。它似乎类似于Paolo.Bolzoni在他的答案中提出的负位域。你可以澄清一下(我需要编辑答案以撤回DV)。 - Kami Kaze
    1
    @KamiKaze,如果你有兴趣的话,我刚刚大量重写和改进了我的答案。现在我终于产生了一个单一的STATIC_ASSERT()宏,它可以在所有版本的C和C++中工作(已经使用gcc编译器进行了测试)!对我来说,最大的突破是想出了一个静态断言,在C++ pre-C++11中也能够工作,因为这是我最新发现之前缺失的部分。 - Gabriel Staples
    1
    你一个人所付出的努力就值得点赞了:D 我会阅读的,谢谢 - undefined
    显示剩余3条评论

    14

    cl

    尽管问题明确提到了gcc,但为了完整起见,这里提供了针对Microsoft编译器的调整方法。

    使用负大小数组typedef并不能说服cl输出一个合理的错误。它只会显示error C2118: negative subscript。在这方面,零宽位域表现更好。由于这涉及到对结构体进行typedef,我们真的需要使用唯一的类型名称。 __LINE__无法胜任 - 可能会在头文件和源文件中的同一行上有COMPILE_TIME_ASSERT(),你的编译将会失败。__COUNTER__来拯救(自从gcc 4.3以来已经支持)。

    #define CTASTR2(pre,post) pre ## post
    #define CTASTR(pre,post) CTASTR2(pre,post)
    #define STATIC_ASSERT(cond,msg) \
        typedef struct { int CTASTR(static_assertion_failed_,msg) : !!(cond); } \
            CTASTR(static_assertion_failed_,__COUNTER__)
    

    现在

    STATIC_ASSERT(sizeof(long)==7, use_another_compiler_luke)
    

    cl 的结果如下:

    错误 C2149: 'static_assertion_failed_use_another_compiler_luke' : 命名的位域不能有零宽度

    Gcc也会给出一个易懂的信息:

    错误:位域“static_assertion_failed_use_another_compiler_luke”的宽度为零


    4

    来自维基百科

    #define COMPILE_TIME_ASSERT(pred) switch(0){case 0:case pred:;}
    
    COMPILE_TIME_ASSERT( BOOLEAN CONDITION );
    

    15
    最好你提供真正的来源链接:http://www.jaggersoft.com/pubs/CVu11_3.html - Matt Joiner
    它在gcc 4.6中无法工作 - 它会显示“case label does not reduce to an integer constant”。它有一点道理。 - Liosan
    你们可能现在都已经远离这个问题了,但是我最终还是自己写了一个(参见我的回答)。我使用了你提供的链接 @MattJoiner 来帮助我。 - Hashbrown
    如果你有兴趣的话,@Liosan,请告诉我它是否适用于你。我刚开始涉足C++,所以我来晚了。 - Hashbrown
    就 Visual C++ 而言,自 2010 版本起便内置了 static_assert ,且其在 c++ 和 c 模式下都能运行。然而,它并不支持 c99 中的 _Static_assert 。 - ddbug

    3
    经典的方式是使用数组:
    char int_is_4_bytes_assertion[sizeof(int) == 4 ? 1 : -1];
    

    它之所以有效,是因为如果断言为真,则数组的大小为1且有效,但如果为假,则大小为-1会导致编译错误。

    大多数编译器将显示变量名并指向代码中正确的部分,您可以在其中留下有关断言的注释。


    1
    将这个封装成一个通用的 #define STATIC_ASSERT() 类型宏,并提供更通用的示例和使用 STATIC_ASSERT() 的示例编译器输出,这将为您赢得更多的赞并使这种技术更有意义。 - Gabriel Staples
    1
    我不同意。编译器看到宏定义时会给出更令人困惑的消息。 - Paolo.Bolzoni
    它可以使用内存... 这不好。 - Samuel

    3
    我不建议使用使用 typedef 的解决方案:
    // Do NOT do this
    #define STATIC_ASSERT(COND,MSG) typedef char static_assertion_##MSG[(COND)?1:-1]
    

    使用typedef关键字的数组声明不能保证在编译时评估。例如,在块作用域中的以下代码将编译:
    int invalid_value = 0;
    STATIC_ASSERT(invalid_value, this_should_fail_at_compile_time_but_will_not);
    

    我建议使用以下方式(在C99中)代替:

    我建议使用以下方式(在C99中)代替:

    // Do this instead
    #define STATIC_ASSERT(COND,MSG) static int static_assertion_##MSG[(COND)?1:-1]
    

    由于使用了static关键字,该数组将在编译时定义。请注意,此断言仅适用于在编译时评估的COND。它将无法处理基于内存中的值(例如分配给变量的值)的条件,因此编译将失败。


    7
    虽然这样做是可行的,但也会增加你的内存需求。 - sherrellbc
    1
    错误:定义了“static_assertion_INVALID_CHAR_SIZE”,但未使用[-Werror=unused-variable]。 - Alex D
    未使用的变量警告可以通过__attribute__((unused))关闭。我将其与typedef解决方案进行了比较,编译器生成了完全相同的代码,很可能是因为该变量未被使用。因此,这不会增加内存需求。 - MarcH

    3
    如果在使用STATIC_ASSERT()宏时加上__LINE__,可以通过包含__INCLUDE_LEVEL__来避免在.c文件中的条目和头文件中的另一个条目之间出现行号冲突。
    例如:
    /* Trickery to create a unique variable name */
    #define BOOST_JOIN( X, Y )      BOOST_DO_JOIN( X, Y )
    #define BOOST_DO_JOIN( X, Y )   BOOST_DO_JOIN2( X, Y )
    #define BOOST_DO_JOIN2( X, Y )  X##Y
    #define STATIC_ASSERT(x)        typedef char \
            BOOST_JOIN( BOOST_JOIN(level_,__INCLUDE_LEVEL__), \
                        BOOST_JOIN(_assert_on_line_,__LINE__) ) [(x) ? 1 : -1]
    

    2

    通用的 static_assert(C89、C++98 或更高版本):

    Github链接

    #define CONCAT_(prefix, suffix)     prefix##suffix
    #define CONCAT(prefix, suffix)      CONCAT_(prefix, suffix)
    
    
    #define outscope_assert(expr)                           \
        struct CONCAT(__outscope_assert_, __COUNTER__)   \
        {                                                   \
            char                                            \
            outscope_assert                                 \
            [2*(expr)-1];                                   \
                                                            \
        } CONCAT(__outscope_assert_, __COUNTER__)
    

    像这样调用:

    outscope_assert( 0 > 5 );
    

    兼容C89:

    gcc main.c -o main.exe -std=c89
    

    或者C++98:

    g++ main.c -o main.exe -std=c++98
    

    它产生的错误看起来像这样:

    main.c:32:9: error: size of array 'outscope_assert' is negative
       32 |         outscope_assert                                 \
          |         ^~~~~~~~~~~~~~~
    main.c:50:1: note: in expansion of macro 'outscope_assert'
       50 | outscope_assert(2 > 5);
          | ^~~~~~~~~~~~~~~
    

    它不像其他提出的解决方案那样容易出现问题,您可以进行测试!

    /* Some test */
    
    /* Global */
    outscope_assert(1 < 2);
    
    /* Within Struct */
    struct A
    {
        int a;
    
        outscope_assert(1 < 2);
        outscope_assert(2 > 1); outscope_assert(2 > 1); /* Same Line */
    };
    
    
    int main (void)
    {
        /* Within Function */
        outscope_assert(2 > 1);
        return 0;
    }
    

    关于__COUNTER__

    __COUNTER__是一个宏,在编译期间单调递增(从0开始),通常用于生成ID。

    该宏可能会根据编译器和/或其版本而定义或未定义,尽管基本上所有现代编译器都支持它。

    如果您被困在使用某些恐龙或独角兽般的编译器上,则必须使用替代方法:

    1. __COUNTER__替换为__LINE__,但问题在于您将无法在同一行上再次使用静态断言,并且在不同文件中也可能遇到问题。
    2. (可能是最佳选项)__COUNTER__替换为my_id,并将其作为宏函数定义的参数添加,如下所示:#define outscope_assert(expr, myid),然后像这样调用宏:outscope_assert(0 < 1 , this_is_myid );
    3. 请阅读下一段,我们将使用inscope_assert

    (可以跳过这部分) 为什么要使用 inscope_assert ?

    #define inscope_assert(expr)                            \
        {                                                   \
            char                                            \
            inscope_assert                                  \
            [2*(expr)-1];                                   \
            (void)inscope_assert;                           \
        }
    

    你可能已经注意到了,inscope_assertoutscope_assert 几乎相同,只是没有了 structid 这些东西。

    这个显然有缺点:它既不能在全局范围内使用,也不能在结构体内使用,但是如果你不能使用 __COUNTER__ 的话,你可以放心使用它,因为它不会产生任何副作用,并且保证了严格的 C89 标准。

    你可能已经注意到(也可能没有),如果我们在编译时使用更严格的标志:

    gcc main.c -o main.exe -std=c89 -Wall -Wextra -ansi -pedantic
    

    我们在 main 声明的 outscope_assert 给我们带来了一个 warning

    warning: unused variable '__outscope_assert__9' [-Wunused-variable]
    

    简单地告诉我们声明了一个名为struct的变量但是没有使用,这并不是什么大问题,但是我们的inscope_assert将无法产生它。


    C11、C23和GCC特定

    C11 standard adds the _Static_assert keyword.
        _Static_assert ( expression , message )
    
    C23 standard adds the static_assert keyword
        static_assert ( expression , message )
    or  static_assert ( expression )
    
    gcc-4.6 adds the _Static_assert keyword for all versions of C,
    including c90, c99, c11, c17, etc.
    

    所以只需进行一些预先检查:

    #if !defined static_assert
    
        /* C11 or later */
        #if defined _Static_assert
            #define static_assert _Static_assert
    
        /* GCC 4.6 or later */
        #elif defined __GNUC__ && ( __GNUC__ > 4 || __GNUC__ == 4 && defined __GNUC_MINOR__ && __GNUC_MINOR >= 6)
            /*  It will work but it will throw a warning:
                warning: ISO C90 does not support '_Static_assert' [-Wpedantic] 
            */
            #define static_assert _Static_assert
        #endif
    
    #endif
    

    快速技术解释:

    展开宏后,它看起来像这样:

    struct __outscope_assert_24 { char outscope_assert [2*(2 > 1)-1]; } __outscope_assert_25
    

    我们的目标是在表达式(在本例中为2>1)为假时抛出错误:
    error: size of array 'outscope_assert' is negative
    

    表达式(2 > 1)的值为1,因为true,所以2*1 - 1 = 1,我们声明了一个大小为1char数组。

    表达式(2 > 9)的值为0,因为false,所以2*0 - 1 = -1,我们声明了一个大小为-1char数组。这将在编译时引发错误。

    __outscope_assert_24__outscope_assert_25是结构体名称和变量名称,省略第一个会在g++中引发警告,省略第二个会导致在同一作用域中声明两个assert时出现重复成员错误。

    CONCATCONCAT_用于创建结构体和变量名称。


    1

    来自Perl,具体来说是perl.h第3455行(之前已经包含了<assert.h>):

    /* STATIC_ASSERT_DECL/STATIC_ASSERT_STMT are like assert(), but for compile
       time invariants. That is, their argument must be a constant expression that
       can be verified by the compiler. This expression can contain anything that's
       known to the compiler, e.g. #define constants, enums, or sizeof (...). If
       the expression evaluates to 0, compilation fails.
       Because they generate no runtime code (i.e.  their use is "free"), they're
       always active, even under non-DEBUGGING builds.
       STATIC_ASSERT_DECL expands to a declaration and is suitable for use at
       file scope (outside of any function).
       STATIC_ASSERT_STMT expands to a statement and is suitable for use inside a
       function.
    */
    #if (defined(static_assert) || (defined(__cplusplus) && __cplusplus >= 201103L)) && (!defined(__IBMC__) || __IBMC__ >= 1210)
    /* static_assert is a macro defined in <assert.h> in C11 or a compiler
       builtin in C++11.  But IBM XL C V11 does not support _Static_assert, no
       matter what <assert.h> says.
    */
    #  define STATIC_ASSERT_DECL(COND) static_assert(COND, #COND)
    #else
    /* We use a bit-field instead of an array because gcc accepts
       'typedef char x[n]' where n is not a compile-time constant.
       We want to enforce constantness.
    */
    #  define STATIC_ASSERT_2(COND, SUFFIX) \
        typedef struct { \
            unsigned int _static_assertion_failed_##SUFFIX : (COND) ? 1 : -1; \
        } _static_assertion_failed_##SUFFIX PERL_UNUSED_DECL
    #  define STATIC_ASSERT_1(COND, SUFFIX) STATIC_ASSERT_2(COND, SUFFIX)
    #  define STATIC_ASSERT_DECL(COND)    STATIC_ASSERT_1(COND, __LINE__)
    #endif
    /* We need this wrapper even in C11 because 'case X: static_assert(...);' is an
       error (static_assert is a declaration, and only statements can have labels).
    */
    #define STATIC_ASSERT_STMT(COND)      STMT_START { STATIC_ASSERT_DECL(COND); } STMT_END
    

    如果<assert.h>中提供了static_assert,则使用它。否则,如果条件为假,则声明一个大小为负的位域,导致编译失败。 STMT_START / STMT_END是宏,分别扩展为do / while (0)

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接