libc6:在assert宏定义中使用逗号操作符

3

我的系统使用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); \
    }))

这个最后的定义存在哪些问题?
2个回答

4
  1. ({不是标准C,会在标准C编译模式下触发警告或错误。
  2. 所以他们使用__extension__,这将禁用任何这样的诊断。
  3. 但是__extension__也会屏蔽expr中的非标准结构,而您确实希望对其进行诊断。
  4. 这就是为什么他们需要将expr重复两次,一次在__extension__内部,一次在外部。
  5. 但是expr只需要计算一次。
  6. 因此,他们通过将expr的另一个出现放在sizeof内来抑制另一个评估。
  7. 只有sizeof(expr)是不够的,因为它对于函数名等事物不起作用。
  8. 因此,改为使用sizeof((expr) ? 1 : 0),这样就不存在这个问题了。
  9. 因此,生成的表达式的两个部分是(a)sizeof((expr) ? 1 : 0)和(b)__extension__(...)部分。
  10. 如果expr出现问题,则仅需要第一部分来产生诊断。
  11. 第二部分执行实际的断言。
  12. 最后,使用逗号运算符连接这两个部分。

4

我不完全确定,但我会尽力而为。

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

  • 逗号运算符丢弃前n-1个表达式结果并返回第n个结果。它经常被用作序列点,因为保证表达式将按顺序评估。

  • 这里使用了__extension__,这是一个GNU LibC宏,用于掩盖在编译环境下指定严格警告(通过-ansi-pedantic等)的头文件中关于GNU特定扩展的任何警告。通常在这样的编译器下,使用特定于编译器的扩展会引发警告(或者如果您在-Werror下运行,这很常见,则会引发错误),但由于在使用GNU库和编译器的情况下,libc允许自己在可以安全地这样做的情况下使用一些扩展。

现在,由于实际的断言逻辑可能使用GNU扩展(如使用__extension__所示),因此表达式本身可能会根据其语义引发实际警告(即传递给assert(expr)的表达式)将被遮蔽,因为该表达式将在__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会导致一个运行时值——特别是在变长数组的情况下——可能会产生不良影响,或者可能会产生错误,例如当传递函数名给sizeof时。
最后,他们希望在实际评估之前保留assert()作为表达式,同时想要所有这些内容。因此,他们没有使用do{}while()块或类似的东西,因为这最终会导致assert()成为语句。相反,他们使用逗号运算符来丢弃第一个sizeof()技巧的结果。

如果我理解正确,由于使用逗号运算符,操作符({})是必需的,对吗?需要列表中的最后一个元素是可以计算为值的表达式。但是当expr为true时,计算的值是什么?我已经编辑了我的问题以获取更多细节。 - mastupristi
({}) 没有什么特殊的地方,除了 __extension__ 是类似函数的标识符(像宏函数一样,即 #define __extension__(...) ...)需要一个代码块。当表达式为真时,我不确定你的意思 - 返回值是 __extension__ 返回的任何内容。我不确定 assert() 的返回值是否被指定,甚至是否可用。 - Qix - MONICA WAS MISTREATED

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