libc6:断言宏定义中的逗号运算符

Posted

技术标签:

【中文标题】libc6:断言宏定义中的逗号运算符【英文标题】:libc6: comma operator in assert macro definition 【发布时间】:2019-05-26 13:52:02 【问题描述】:

我的系统使用 libc6 2.29。在/usr/include/assert.h可以找到assert()宏的定义:

/* The first occurrence of EXPR is not evaluated due to the sizeof,
   but will trigger any pedantic warnings masked by the __extension__
   for the second occurrence.  The ternary operator is required to
   support function pointers and bit fields in this context, and to
   suppress the evaluation of variable length arrays.  */
#  define assert(expr)                          \
  ((void) sizeof ((expr) ? 1 : 0), __extension__ (         \
      if (expr)                             \
        ; /* empty */                           \
      else                              \
        __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION);   \
    ))

我想知道为什么要使用逗号运算符,以及“The first occurrence of EXPR is not evaluated due to the sizeof”是什么意思。

使用以下定义会有什么问题:

#  define assert(expr)                      \
  (                                        \
      if (expr)                             \
           ; /* empty */                            \
      else                              \
           __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION);    \
    )

编辑:

如果expr 为真,运算符 ( ) 会得到什么值?

是否可以将assert()的定义改写如下?

#  define assert(expr)                          \
  ((void) sizeof ((expr) ? 1 : 0), __extension__ (         \
      if (!expr)                                \
          __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION); \
    ))

最后一个定义的问题在哪里?

【问题讨论】:

【参考方案1】:

我不是 100% 确定这一点,但我会试一试。

首先,让我们回顾一下这里使用的一些东西。

逗号运算符丢弃前 n-1 个表达式结果并返回第 n 个结果。它通常用作sequence point,因为它保证表达式将按顺序计算。

这里使用__extension__,它是一个GNU LibC 宏,用于屏蔽在编译环境下通过-ansi 或@987654326 指定迂腐警告的头文件中有关GNU 特定扩展的任何警告@ 等。通常在此类编译器下,使用特定于编译器的扩展会引发警告(如果您在 -Werror 下运行,则会引发错误,这很常见),但因为在 GNU 库和编译器正在运行的情况下使用时,libc 允许自己使用一些可以安全使用的扩展。

现在,由于实际的断言逻辑可能使用 GNU 扩展(正如使用 __extension__ 所表明的那样,根据其语义,表达式本身可能引发的任何真正警告(即传递给 @987654329 的表达式) @) 将被屏蔽,因为该表达式将在语义上位于 __extension__ 块内并因此被屏蔽。

因此,需要有一种方法让编译器有机会显示这些警告,但不计算实际表达式(因为表达式可能有副作用,双重计算可能会导致不良行为)。

您可以通过使用 sizeof 运算符来做到这一点,该运算符接受一个表达式,查看它的类型,并找到它占用的字符数 - 而无需实际评估表达式。

例如,如果我们有一个函数int blow_up_the_world(),那么表达式sizeof(blow_up_the_world()) 将在不实际计算表达式的情况下找到表达式结果的大小(在本例中为int)。在这种情况下使用sizeof() 意味着世界实际上不会被炸毁。

但是,如果传递给 assert(expr)expr 包含会触发编译器警告的代码(例如,使用 -pedantic-ansi 模式下的扩展),编译器仍会显示这些警告,即使代码位于 sizeof() 内 - 否则将在 __extension__ 块内隐藏的警告。

接下来,我们看到他们没有将expr 直接传递给sizeof,而是使用三元组。这是因为三元的类型是两个结果表达式所具有的任何类型 - 在这种情况下是 int 或等效的东西。这是因为将某些内容传递给 sizeof 将导致 runtime 值 - 即在 variable length arrays 的情况下 - 可能产生不良影响,或者可能产生错误,比如passing sizeof a function name时。

最后,他们想要所有这些,但在实际评估之前,并希望将 assert() 保留为表达式,因此不要使用 dowhile() 块或类似的东西,这最终会导致 assert() 成为声明,他们改为使用逗号运算符丢弃第一个 sizeof() 技巧的结果。

【讨论】:

如果我理解正确,由于使用了逗号操作符,操作符 ( ) 是必要的,对吧?需要列表中的最后一个元素是可以计算为值的表达式。但是当 expr 为真时评估的值是多少?我已经编辑了我的问题以获取更多详细信息 () 没有什么特别之处,除了 __extension__ 是一个类似函数的标识符(如宏函数,即#define __extension__(...) ...),它需要一个块。我不确定当表达式为真时您的意思是什么 - 返回值是 __extension__ 返回的任何值。我不确定assert()的返回值是否指定,甚至是否可用。【参考方案2】:
    ( 不是标准 C,会在标准 C 编译模式下触发警告或错误。 所以他们正在使用__extension__,这将禁用任何此类诊断。 但是,__extension__ 也会屏蔽 expr 中的非标准构造,您确实希望对其进行诊断。 这就是为什么他们需要 expr 重复两次,一次在 __extension__ 内部,一次在外部。 但是expr 只需要评估一次。 因此他们通过将另一个出现的expr 放在sizeof 中来抑制另一个评估。 只是sizeof(expr) 是不够的,因为它不适用于函数名称之类的东西。 所以改用sizeof((expr) ? 1 : 0),就没有这个问题了。 所以生成的表达式的两部分是 (a) sizeof((expr) ? 1 : 0) 和 (b) __extension__(...) 部分。 只有在expr 出现问题时才需要第一部分进行诊断。 第二部分执行实际的断言。 最后用逗号将两部分连接起来。

【讨论】:

以上是关于libc6:断言宏定义中的逗号运算符的主要内容,如果未能解决你的问题,请参考以下文章

编译器如何知道函数调用中的逗号不是逗号运算符?

JavaScript 逗号运算符

添加数字千分位逗号正则分析

js中的逗号运算符

为啥逗号运算符在运算符[]内部称为逗号运算符而不在运算符()内部调用?

C ++中的运算符逗号?:条件