【问题标题】:Different behaviour of C macro for different cases不同情况下 C 宏的不同行为
【发布时间】:2015-07-18 18:22:10
【问题描述】:

这是代码,

#include<stdio.h>
#include<stdbool.h>

#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))                                                               

struct my_struct {
    int a, b;
//  char c;
};

int main() {
    bool cond = 1;
    BUILD_BUG_ON((sizeof(struct my_struct) % 8) != 0);
    BUILD_BUG_ON(cond);
    return 0;
}

如果结构体 sizeof 不等于 8,则首次使用 BUILD_BUG_ON(condition) 宏会引发编译错误,因为条件将评估为真。但是即使我提供了真实的条件,第二次使用宏也不会引发编译错误。我无法理解这种行为。谁能解释一下?

【问题讨论】:

  • 似乎条件需要是编译时常量表达式才能使该错误“起作用”。
  • printf("%ld\n", sizeof(char[1 - 2 * !!(cond)])); 给出-1。我不知道这是可能的。
  • 此外,当条件动态为真时,此代码只是具有未定义的行为,因为可变长度数组的大小必须大于零(参见 6.7.6.2/5)。
  • 您似乎忽略了 BUILD_BUG_ON 和普通 assert() 之间的区别。

标签: c


【解决方案1】:

BUILD_BUG_ON 宏旨在实现编译时断言。

给定一个可以在编译时求值的参数,如果参数为非零 (true),则会导致编译时失败,如果参数为非零 (false),则不执行任何操作。

它不适用于在运行时评估的参数。

#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))

!! 是两个逻辑“非”运算符;它们的作用是将0 的值标准化为0,并将任何非零值标准化为1

如果结果条件为1 (true),则1 - 2*!!(condition) 的值为-1。如果条件为0(false),则值为1

数组的大小不得为负(或零)。一些编译器可能支持零长度数组作为扩展;这个宏确保即使是这样的编译器也能诊断出错误。如果大小是常量表达式,则具有负大小的数组是违反约束的,需要编译时诊断。

如果表达式为假,则没有错误;宏扩展为一个什么都不做的表达式。如果表达式为真并且是一个常量表达式,那么宏的扩展会尝试定义一个负大小的数组,从而导致编译时错误。

如果表达式不是常量,则宏不起作用。 C(C99 和更高版本)允许可变长度数组(VLA)。零长度或负长度的 VLA 是不允许的,但定义这样的 VLA 通常不能在编译时检测到。这是未定义的行为——在这种情况下,它可能什么都不做。 (只是为了使事情复杂化,文件范围内不允许使用 VLA。)

理想情况下,宏应附有说明如何使用它的文档。该文档应说明参数必须是编译时表达式。

底线:您应该只将此宏与常量表达式参数一起使用。 (要测试运行时表达式,您可以使用assert()。)如果您使用具有零值的非常量表达式,则行为未定义;最可能的结果是预期的“断言”不会触发并且不会检测到错误。

【讨论】:

  • 是否有任何要求 sizeof 必须在编译时进行评估?我用过的所有编译器都这样做,但它是由标准强制执行的吗?
  • @BlueMoon:是的,对于非 VLA 操作数。 N1570 6.5.3.4p2:“如果操作数的类型是可变长度数组类型,则计算操作数;否则,不计算操作数,结果为整数常量。”
  • 我的意思是,如果一个假设的编译器,例如,在运行时评估 sizeof(int),它是否违反了 C 标准的任何约束?
  • @BlueMoon:仅当它影响程序的行为时。该标准说它是一个“整数常数”。 (它会如何在运行时评估它?)
  • @grenix 是的,但这种情况并不愚蠢。在很多情况下,您想要测试条件是“真”还是“假”,其中“真”被定义为非零。 isdigit() 和朋友就是一个例子;它们被定义为返回非零值,不一定是1,用于匹配参数。那是运行时,但可能存在类似的编译时情况。
【解决方案2】:

改变

BUILD_BUG_ON(cond);

BUILD_BUG_ON(1);

获得预期的行为。

如果 cond 值仅在运行时可用,则在未优化的构建中生成计算所需 sizeof 的代码(并丢弃其结果),或者在优化的构建中完全忽略整个表达式。

【讨论】:

    【解决方案3】:

    我认为你的方法完全不自然。
    这是糟糕的代码,即使它会起作用。
    您可以尝试更自然的方法来处理错误。

    断言

    assert 在运行时测试条件,如果测试失败,则会显示错误消息。

     #include <assert.h>
     int main(void)
     {
         assert((sizeof(struct my_struct) % 8) != 0);
     }
    

    static_assert

    在 C11 投诉编译器中,static_assert(condition, "Error message") 对常量表达式执行您想要的操作:

    static_assert((sizeof(struct my_struct) % 8) != 0, "Wrong struct");  
    

    #if 和 #error

    也可以使用#if#error 编译器指令,但在这种情况下我不建议你,因为sizeof 不被#if 理解。

    语法是:

    #if some_condition // Put an integer constant expression readable by an #if.
    #  error This is wrong.
    #endif
    

    这三种方法中的一些必须满足您的需求。

    【讨论】:

    • 这三种方法完全有可能都不行。 assert 在运行时评估; OP 希望在编译时检测到错误。 static_assert 是最近才添加的; OP 很可能正在使用不支持它的编译器。 #if#error,如你所说,不认识sizeof。我见过用于模拟编译时断言的类似技术。它们很丑陋,但丑陋在宏定义中是孤立的。这是一种完全合法的技术。此外,您还没有回答 OP 的问题,即代码是如何工作的。
    • 你可以称之为坏代码,但即使是 linux 内核也使用类似的技巧进行编译时断言。
    • @HolyBlackCat:你陷入了诉诸权威的谬误。
    • @pablo1977 抱歉,我的措辞并不完全清楚。我并不是说 OP 的代码很好。但是这个技巧之王是在 C11 之前的编译时断言的唯一方法。 linux内核中存在类似代码的事实只是表明它已用于某些实际软件中。虽然 OP 的方法是不可避免的,但可以改进他的代码。我会将其重写为#define BUILD_BUG_ON(x) do {struct {int:-!!(x);} a; sizeof a;} while (0)。此版本在 C++ 中工作,如果在表达式中使用非 const 值,则会发出错误。
    • 这似乎比负数组方法更好,主要是因为它不允许运行时评估(如前所述)。为避免有关未使用值 (GCC) 的警告,请将 sizeof 的结果强制转换为 void,即 #define BUILD_BUG_ON(x) do {struct {int:-!!(x);} a; (void) sizeof a;} while (0)
    猜你喜欢
    • 2013-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多