为什么 (*p=*p) & (*q=*q);在 C 中触发未定义的行为

Why does (*p=*p) & (*q=*q); in C trigger undefined behavior

如果 pq 相等,为什么 C 中的 (*p=*p) & (*q=*q); 会触发未定义的行为。

int f2(int * p, int * q)
{
  (*p=*p) & (*q=*q);
  *p = 1;
  *q = 2;
  return *p + *q;
}

来源(顺便说一句,文章不错):http://blog.frama-c.com/index.php?post/2012/07/25/On-the-redundancy-of-C99-s-restrict

如果 *p*q 指定相同的内存位置,则在没有插入序列点(或 C11 中的序列关系)的情况下写入它们会导致未定义的行为。

=&不引入序列点。

该代码等同于 int i = 0; (i=i) & (i=i);,出于同样的原因,它具有 UB。另一个类似的例子是 (*p = 1) & (*q = 2).

C11标准对声明的裁决

(*p=*p) & (*q=*q);

是:

P1

§6.5p3

The grouping of operators and operands is indicated by the syntax. 85) Except as specified later, side effects and value computations of subexpressions are unsequenced.

由于 §6.5.10 按位与运算符 未提及其操作数的排序,因此 (*p=*p)(*q=*q) 未测序。

P2

§6.5p2

If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined. If there are multiple allowable orderings of the subexpressions of an expression, the behavior is undefined if such an unsequenced side effect occurs in any of the orderings. 84)

两个赋值 (*p=*p)(*q=*q) 都是未排序的 w.r.t。 §6.5p3 中的彼此,如果 p==q 则对同一对象产生副作用。因此,如果 p==q,那么根据 §6.5p2 我们有 UB。

P3

§3.4.3

undefined behaviour

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements.

通过这个条款我们知道标准对UB没有要求。这通常被编译器解释为允许忽略发生此类行为的可能性。

特别是,它允许编译器不处理 p == q 的情况,这意味着它可能假设 p != q.

P1+P2+P3 -> C1

因为(*p=*p)(*q=*q)可以假设组合前提P1、P2和P3不调用UB,所以它们也可以假设为加载和存储到不同的内存位置。这也意味着 f2 的 return 值必须是 3 而不是 4。如果 p == q,则标准对发生的情况没有任何要求。

简单来说,如果 pq 具有相同的值,则 (*p = *p) & (*q = *q) 未定义,因为:

  • 您不能在未排序的评估中对同一位置进行两次突变;和
  • 您无法从同一未排序求值中发生变异的位置读取数据。

这在 C 和 C++ 中都是未定义的行为,尽管标准措辞略有不同(并且上面的文本与任何一个标准都不完全对应;它旨在作为一个简化的解释。我相信你可以在 SO 上找到精确的文本。)

& 运算符是一个简单的按位 and,因此它不强加任何计算顺序。看起来 *p = *p 是一个明显的空操作,但不能保证它是以这种方式实现的。编译器可以(例如)将其实现为 tmp = *p; *p = 0; *p += tmp。它也可能无法一次设置 *p 的所有位,需要逐步完成分配。


现在,有点个人问题。表达式 <something>触发 未定义的行为”听起来好像有某种行为类别称为 "undefined behaviour",也许是一种将开始触发的大红色按钮鼻魔向四面八方施压时。对于正在发生的事情,这不是一个好的模型。最好说 "the behaviour of <something> is undefined".

请注意,如果执行的程序的任何部分具有未定义的行为,则整个程序的行为是未定义的。 整个程序,而不是程序中以未定义行为开始的部分。


最后——这是链接文章的重点——允许编译器假定程序的行为已定义。因此,如果程序包含像 (*p = *p) & (*q = *q) 这样的表达式,则编译器可以假定 pq 指向不同的非重叠对象。一旦做出该假设,它可能能够更好地优化涉及 *p 和 *q 的表达式。也有可能一旦编译器做出该假设,它就可以消除 (*p = *p) & (*q = *q) 的整个计算,因为如果 p 和 q 是清楚的。因此,您可以将该表达式视为一种 声明 :您向编译器承诺您已采取一切必要措施来保证 p 和 q 指向不同的非重叠对象。 (编译器不会,也可能无法验证您的声明。它只会相信您的话。)

然后作者认为这个成语比(有点争议的)restrict关键字更强大。我毫不怀疑它是,而且很可能构建这样的表达式来涵盖许多无法用 restrict 轻松表达的限制。所以在某种程度上,这似乎是一个有趣的想法。另一方面,精确的表达至少可以说是晦涩难懂且容易出错。

该线程的问题以 "Why does (*p=*p) & (*q=*q); in C trigger undefined behavior if p and q are equal?" 开头,提问者引用了一篇文章,该文章指出 C(和 C++?)中新的 restrict 关键字是不必要的,因为我们可以告诉编译器这通过写一个表达式 (*p=*p) & (*q=*q);.

用户Iwillnotexist Idonotexist对这个表达的解释非常透彻...也非常复杂。基本上,结论是这更像是一个 指令 而不是 语句 因为表达式不产生任何使用的结果并且只有副作用(分配给 self)没有效果(self 保持不变,even if p==q),所以任何好的编译器都可以优化它。

还是没有完全理解解释,我选择了那个新关键字而不是写错了表达式。

编写 C 标准时,如果某个动作的效果在不同平台上会有所不同,则特定平台并不总是能够保证任何特定的精确效果,并且如果可能存在合理的实现该操作可能会触发一个硬件陷阱,其行为超出了 C 编译器的控制范围,因此让标准说明该行为的任何内容都没有什么可感知的价值。即使硬件陷阱的可能性不大,"surprising" 行为的可能性也足以将行为标记为未定义。

例如,考虑 unsigned long x,*p; ... *p=(x++);。如果 p==&x,那么 *p 不仅有可能最终不仅持有 x 的旧值,或者更大的值 1,而且如果 x 是,例如0x0000FFFF 它也可能最终持有 0x00000000 或 0x0001FFFF。即使没有机器会触发硬件陷阱,我认为标准的作者也不会认为 "Any lvalue modified more than once will hold Indeterminate Value, and any read of an lvalue in the same expression that writes it in a manner other than allowed herein may yield Indeterminate Value" 比简单地将此类操作声明为未定义行为更有用。此外,从标准作者的角度来看,标准未能在某些平台可以免费提供而其他平台不能免费提供的情况下强制执行特定行为预计不会对平台上此类行为的规范造成障碍可以提供它们。

在实践中,即使是非常松散指定的行为通常对于与当今编写的绝大多数程序共享以下两个要求的程序也非常有用:

  1. 当给定有效输入时,产生正确的输出。
  2. 输入无效时,不发射核导弹。

不幸的是,有人提出了这样的想法,即如果 C 标准不强制要求某些动作 X 在特定情况 Y 中的行为,即使大多数编译器恰好具有足以满足程序要求的行为上述要求(例如,即使 pq 识别不相关的对象,大多数编译器将为表达式 p < q 生成的代码将产生 0 或 1 并且没有其他副作用) ,那么动作 X 应该被视为向编译器表明程序永远不会接收任何会导致情况 Y 的输入。

所指示的(*p=*p) & (*q=*q)旨在表示这样一个"promise"。逻辑是,由于标准不会说明如果 p==q 编译器可以做什么,编译器应该假设程序员不会介意程序是否会发射核导弹以响应任何可能的输入导致代码在 p==q.

时执行

这个想法及其后果从根本上与 C 的本质和设计目标及其使用系统编程语言背道而驰。几乎所有系统都提供一些超出标准强制要求的功能和保证,尽管具体情况因系统而异。我认为通过将 x < y 从 "I'm willing to accept whatever means of pointer comparison is used by any hardware on which this program is actually going to be run" 重新定义为 "I'm so certain that these two pointers will be related that I would stake my life on it" 比通过添加一种新方法来指导编译器假定 [=47] 更好地服务于语言的想法是荒谬的=], 但不知何故它似乎正在被接受。