未定义的行为和序列点

什么是“序列点”?

未定义的行为和序列点之间的关系是什么?

我经常使用像a[++i] = i;这样有趣而复杂的表达a[++i] = i; ,让自己感觉更好。 我为什么要停止使用它们?

如果您已阅读此内容,请务必访问后续问题重新加载未定义的行为和序列点

(注意:这是Stack Overflow的C ++常见问题解答的一个条目。如果你想批评在这种forms下提供常见问题解答的想法,那么发布所有这些的meta上的post就是这样做的地方。这个问题在C ++聊天室中受到监控,其中FAQ的想法首先出现在那里,所以你的答案很可能被那些提出想法的人阅读。)

C ++ 98和C ++ 03

这个答案适用于旧版本的C ++标准。 该标准的C ++ 11和C ++ 14版本没有正式包含“序列点”; 操作是“先排序”或“未排序”或“不确定排序”。 净效果基本相同,但术语不同。


免责声明 :好的。 这个答案有点长。 所以在阅读时要有耐心。 如果你已经知道这些事情,再次阅读它们不会让你发疯。

先决条件 : C ++标准的基础知识


什么是序列点?

标准说

在称为序列点的执行序列中的某些特定点 ,先前评估的所有副作用应是完整的,并且不会发生后续评估的副作用 。 (§1.9/ 7)

副作用? 有什么副作用?

表达式的评估产生一些东西,如果另外执行环境的状态发生变化,则表示表达式(其评估)具有一些副作用。

例如:

 int x = y++; //where y is also an int 

除了初始化操作之外,由于++运算符的副作用, y的值也会发生变化。

到现在为止还挺好。 继续前进到序列点。 comp.lang.c作者Steve Summit给出的seq-points的交替定义:

序列点是尘埃落定的时间点,到目前为止所见的所有副作用都保证完整。


C ++标准中列出了哪些常见的序列点?

那些是:

  • 在完整表达式的评估结束时( §1.9/16 )(完整表达式是一个不是另一个表达式的子表达式的表达式。) 1

示例:

 int a = 5; // ; is a sequence point here 
  • 在评估第一个表达式( §1.9/18 )后评估下列每个表达式2

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18) (这里a,b是逗号运算符;在func(a,a++) ,不是逗号运算符,它只是参数aa++之间的分隔符。因此,在这种情况下,行为是未定义的(如果a被认为是原始类型))
  • 在函数体( §1.9/17 )中执行任何表达式或语句之前发生的所有函数参数(如果有)的评估之后,在函数调用(函数是否内联)之后。

1:注意:对完整表达式的评估可以包括对词性表达式的评估,这些子表达式不是词法表达式的全部表达式。 例如,在评估默认参数表达式(8.3.6)中涉及的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

2:所指示的运算符是内置运算符,如第5节所述。当其中一个运算符在有效上下文中重载(第13节),从而指定用户定义的运算符函数时,表达式指定函数调用和操作数形成一个参数列表,它们之间没有隐含的序列点。


什么是未定义的行为?

标准将第§1.3.12节中的未定义行为定义为

行为,例如在使用错误的程序结构或错误数据时可能出现的行为,本国际标准没有规定3

当本国际标准省略任何明确的行为定义的描述时,也可能会出现未定义的行为。

3:允许的未定义行为范围从完全忽略不完全结果的情况,到翻译或程序执行期间的行为,以文件的特征环境(有或没有发出诊断消息),终止翻译或执行(发布诊断消息)。

简而言之,未定义的行为意味着任何事情都可能发生在从你鼻子飞到你女朋友怀孕的守护进程中。


未定义行为和序列点之间的关系是什么?

在开始之前,您必须了解未定义行为,未指定行为和实现定义行为之间的差异。

您还必须知道, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified

例如:

 int x = 5, y = 6; int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first. 

另一个例子。


现在§5/4的标准说

  • 1) 在前一个和下一个序列点之间,标量对象应通过表达式的计算至多修改其存储值一次。

这是什么意思?

非正式地,它意味着在两个序列点之间不能多次修改变量。 在表达式语句中, next sequence point通常位于终止分号处,而previous sequence point位于前一个语句的末尾。 表达式还可以包含中间sequence points

从上面的句子中,以下表达式调用未定义的行为:

 i++ * ++i; // UB, i is modified more than once btw two SPs i = ++i; // UB, same as above ++i = 2; // UB, same as above i = ++i + 1; // UB, same as above ++++++i; // UB, parsed as (++(++(++i))) i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs) 

但是下面的表达式很好:

 i = (i, ++i, 1) + 1; // well defined (AFAIK) i = (++i, i++, i); // well defined int j = i; j = (++i, i++, j*i); // well defined 

  • 2) 此外,只能访问先前值以确定要存储的值。

这是什么意思? 这意味着如果一个对象被写入一个完整的表达式,那么在同一个表达式中对它的任何和所有访问都必须直接参与计算要写入的值

例如,在i = i + 1i所有访问(在LHS和RHS中) 直接涉及要写入的值的计算 。 所以很好。

此规则有效地将法律表达式约束为在修改之前明显存在访问的表达式。

例1:

 std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2 

例2:

 a[i] = i++ // or a[++i] = i or a[i++] = ++i etc 

是不允许的,因为i的一个访问( a[i]中的a[i] )与最终存储在i中的值无关(在i++发生),因此没有好的方法来定义 -要么是我们的理解,要么是编译器 – 是否应该在存储增量值之前或之后进行访问。 所以行为是不确定的。

例3:

 int x = i + i++ ;// Similar to above 

在这里跟进C ++ 11的答案。

这是我之前的答案的后续内容,包含C ++ 11相关资料。


先修课程 :关系(数学)的基础知识。


在C ++ 11中没有序列点是真的吗?

是! 这是非常正确的。

序列点已被C ++ 11中的Sequenced BeforeSequenced After (以及UnsequencedIndeterminately Sequenced ) 关系所取代。


究竟是什么’之前排序’的事情?

排序之前 (§1.9/ 13)是一种关系,它是:

  • 非对称
  • 及物

在由单个线程执行的评估之间并引发严格的部分顺序 1

forms上它意味着给出任何两个评估(见下文) AB ,如果A B 之前排序 ,那么A的执行应该在 B的执行之前 。 如果AB之前没有排序, BA之前没有排序,则AB未排序2

当在A之前对A进行测序或在A之前A B进行测序之前对A 测序时,评估AB不确定 A ,但是未指定哪个为3

[笔记]
1:严格的偏序是在集合P二元关系 "<" ,它是asymmetric ,并且是transitive ,即对于P所有abc ,我们得到:
........(一世)。 如果a asymmetry );
……..(II)。 如果a transitivity )。
2: 未经测试的评估的执行可能重叠
3: 不确定顺序的评估不能重叠 ,但可以先执行。


在C ++ 11的上下文中,“评估”一词的含义是什么?

在C ++ 11中,表达式(或子表达式)的评估通常包括:

  • 值计算 (包括确定glvalue评估对象的标识并获取先前分配给对象以进行prvalue评估的值 )和

  • 引发副作用

现在(§1.9/ 14)说:

要评估下一个全表达式相关联的每个值计算和副作用之前 ,对与全表达相关联的每个值计算和副作用进行排序

  • 琐碎的例子:

    int x; x = 10; ++x;

    x = 10;的值计算和副作用之后,对与++x相关的值计算和副作用进行排序x = 10;


因此,未定义行为与上述事物之间必然存在某种关系,对吧?

是! 对。

在(§1.9/ 15)中已经提到过

除非另有说明,否则对单个算子的操作数和个别表达式的子表达式的评估是不合理的 4

例如 :

 int main() { int num = 19 ; num = (num << 3) + (num >> 3); } 
  1. +运算符的操作数的评估相对于彼此是不确定的。
  2. <<>>运算符的操作数的评估相对于彼此是无序的。

4:在程序执行期间不止一次评估的表达式中,不需要在不同的评估中一致地执行其子表达式的未序列不确定顺序的评估。

(§1.9/ 15)运算符操作数的值计算在运算符结果的值计算之前排序。

这意味着在x + y中, xy的值计算在(x + y)的值计算之前被排序。

更重要的是

(§1.9/ 15)如果标量对象的副作用相对于其中任何一个都没有排序

(a) 对同一标量物体的另一个副作用

要么

(b) 使用相同标量对象的值进行值计算。

行为未定义

例子:

 int i = 5, v[10] = { }; void f(int, int); 
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

当调用函数时(无论函数是否为内联函数),在执行每个表达式或语句之前,每个值计算和与任何参数表达式相关联的副作用,或者与指定被调用函数的后缀表达式相关联,都会被排序。叫function。 [ 注意: 与不同参数表达式相关的值计算和副作用未被排序 。 - 结束说明 ]

表达式(5)(7)(8)不会调用未定义的行为。 有关更详细的说明,请查看以下答案。

  • 对C ++ 0x中的变量进行多次预增量操作
  • 未测序的价值计算

最后的说明

如果您发现post中有任何缺陷,请发表评论。 高级用户(代表> 20000)请不要犹豫,编辑post以纠正拼写错误和其他错误。

C ++ 17N4659 )包括一个提出用于 N4659 C ++的 精炼表达评估顺序的提议,它定义了更严格的表达式评估顺序。

特别是,增加了以下句子

8.18赋值和复合赋值运算符
….

在所有情况下,在右和左操作数的值计算之后,以及在赋值表达式的值计算之前,对赋值进行排序。 右操作数在左操作数之前排序。

它使以前未定义的行为的几个案例有效,包括有问题的行为:

 a[++i] = i; 

然而,其他几个类似的案例仍会导致未定义的行为。

N4140

 i = i++ + 1; // the behavior is undefined 

但在N4659

 i = i++ + 1; // the value of i is incremented i = i++ + i; // the behavior is undefined 

当然,使用符合C ++ 17的编译器并不一定意味着应该开始编写这样的表达式。

我猜这个改变有一个根本原因,让旧的解释更清晰,不仅仅是装饰性的:原因是并发性。 未详细说明的顺序仅仅是选择几个可能的连续排序中的一个,这与排序之前和之后完全不同,因为如果没有指定的排序,则可以进行并发评估:旧规则不是这样。 例如:

 f (a,b) 

先前要么是b,要么是b,然后是a。 现在,可以使用交错的指令或甚至在不同的核上评估a和b。

C99(ISO/IEC 9899:TC3) ,到目前为止,在该讨论中似乎没有关于评估顺序的下列关注者。

[…]子表达式的评估顺序和副作用发生的顺序都是未指定的。 (第6.5节第67页)

操作数的评估顺序未指定。 如果尝试修改赋值运算符的结果或在下一个序列点之后访问它,则行为[sic]未定义。(第6.5.16节第91页)