未定义的行为和序列点

什么是“序列点”?

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

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

如果您已经阅读了这篇文章,请务必访问后续问题重新加载未定义的行为和序列点

(注意:这是Stack Overflow的C++FAQ的一个条目。如果你想批评以这种形式提供FAQ的想法,那么Meta上的帖子引发了这一切就是这样做的地方。这个问题的答案在C++聊天室中被监控,FAQ的想法最初是在那里开始的,所以你的答案很可能会被提出这个想法的人阅读。)
121809 次浏览

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点的交替定义:

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


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

它们是:

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

    示例:

    int a = 5; // ; is a sequence point here
  • 在计算第一个表达式(§1.9/182之后的以下每个表达式的计算中

    • 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条)从而指定用户定义的运算符函数时,表达式指定函数调用,操作数形成参数列表,它们之间没有隐含的序列点。


什么是未定义行为?

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

行为,例如在使用错误的程序构造或错误的数据时可能出现的行为,本国际标准对此施加了无要求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 SPsi = ++i;     // UB, same as above++i = 2;     // UB, same as abovei = ++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 definedint j = i;j = (++i, i++, j*i); // well defined

    1. 此外,只能访问先验值以确定要存储的值。

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

例如,在i = i + 1中,i的所有访问(在L. H. S和R. H. S中)都是要写入的值的直接参与计算。所以没问题。

这一规则有效地将法律表述限制在访问明显先于修改的那些表述中。

示例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]中的一次)与最终存储在i中的值无关(发生在i++中),因此无论是对我们的理解还是编译器来说,都没有好的方法来定义访问应该发生在增量值存储之前还是之后。所以行为是未定义的。

示例3:

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

C++11

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


先决条件:关系的基本知识(数学)。


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

耶!这是真的。

序列点在C++11中被之前排序排序后(以及未排序不确定顺序关系取代。


这个“之前排序”到底是什么?

之前排序(§1.9/13)是一个关系:

在由单个线程执行的计算之间并诱导严格偏序1

形式上,它意味着给定任意两个评估B2AB,如果AB3B,那么AB4的执行就是B的执行。如果A没有排在B之前,而B没有排在A之前,那么AB就是B5B6。

AB之前排序或BA之前排序时,评估AB不确定排序,但未指定哪个3

[注释]
1:严格偏序是<强>二元关系"<"在集合P上是#2P0,即对于P中的所有abc,我们有:
………(i).如果aasymmetry);
………(ii).如果atransitivity).
2:无序评价的执行可以重叠
3:不确定顺序的评价不能重叠,但可以先执行。


"评价"一词在C++11中的含义是什么?"

在C++11中,对表达式(或子表达式)的求值一般包括:

现在(§1.9/14)说:

与全表达式相关联的每个值计算和副作用都是之前测序,与下一个要计算的全表达式相关联的每个值计算和副作用。

  • 简单的例子:

    int x;x = 10;++x;

    ++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)

调用函数时(无论函数是否内联),与任何参数表达式或指定被调用函数的后缀表达式相关联的每个值计算和副作用都在执行被调用函数主体中的每个表达式或语句之前进行排序。[备注:与不同参数表达式相关的值计算和副作用是无序的.-尾注]

表达式(5)(7)(8)不调用未定义的行为。查看以下答案以获得更详细的解释。


最后说明

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

我猜这一变化有一个根本原因,让旧的解释更清晰不仅仅是表面上的:那个原因是并发。未指定的阐述顺序只是选择了几个可能的序列顺序之一,这与排序之前和之后有很大不同,因为如果没有指定的顺序,并发评估是可能的:旧规则不是这样。例如:

f (a,b)

以前要么a然后b,要么b然后a。现在,a和b可以通过交错指令甚至在不同的内核上进行评估。

C++17N4659)包括一个提案精炼习语C++的表达式评估顺序它定义了更严格的表达式求值顺序。

特别是下面的句子

8.18赋值和复合赋值操作符
……

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

连同下列澄清

表达式X被称为在表达式Y之前排序,如果每个值计算和与表达式X相关的每个副作用都在每个值之前进行排序计算和与表达式Y相关的每一个副作用。

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

a[++i] = i;

然而,其他一些类似的情况仍然导致未定义的行为。

N4140

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

但在N4659

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

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

C99(ISO/IEC 9899:TC3)中,到目前为止,这个讨论中似乎没有出现以下关于评估顺序的讨论。

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

操作数的计算顺序未指定。如果尝试用于修改赋值操作符的结果或访问它在下一个序列点之后,行为[原文如此]是未定义的。(第6.5.16 pp 91)