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

时间:2019-05-26 13:52:03

标签: c macros comma

我的系统使用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 个答案:

答案 0 :(得分:4)

我不确定100%,但我会尝试一下。

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

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

  • 此处使用__extension__(这是一个GNU LibC宏),用于通过{{1}来掩盖在指定脚警告的编译环境下有关标头中特定于GNU扩展的任何警告。 }或-ansi等。通常在此类编译器下,使用特定于编译器的扩展名将引发警告(如果在-pedantic下运行,则会出错,这很常见),但是由于在使用GNU库和编译器的情况下,libc允许自己使用一些可以安全使用的扩展。

现在,由于实际的断言逻辑可能使用了GNU扩展名(如使用-Werror所示,因此表达式本身可能已经提出了任何真正的警告,因为给出了其语义(即,将表达式传递给了__extension__)将被屏蔽,因为该表达式在语义上位于assert(expr)块内,因此被屏蔽。

因此,需要有一种方法使编译器有机会显示这些警告,但无需评估实际的表达式(因为该表达式可能会产生副作用,并且双重评估可能会导致不良行为)。

您可以使用__extension__运算符来执行此操作,该运算符可以获取表达式,查看其类型并查找其占用的字符数-无需实际评估表达式。

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

但是,如果传递给sizeof()的{​​{1}}包含可能触发编译器警告的代码(例如,在exprassert(expr)模式下使用扩展名),则编译器即使代码位于-pedantic内,仍会显示这些警告-否则这些警告会在-ansi块内被屏蔽。

接下来,我们看到它们不是使用sizeof()直接传递给__extension__而是使用三元数。这是因为三元数的类型是两个结果表达式都具有的类型-在这种情况下为expr或类似的东西。这是因为将某些内容传递给sizeof会导致 runtime 值-即在variable length arrays的情况下-可能会产生不良影响,或可能会产生错误,例如passing sizeof a function name

最后,他们想要所有这些,但是在实际评估之前,他们想要保留int作为表达式,因此不要使用sizeof块或类似的东西,最终会导致{ {1}}是一条语句,他们改用逗号运算符丢弃第一个assert()技巧的结果。

答案 1 :(得分:3)

  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. 最后,这两个部分与逗号运算符相连。