未定义的行为和顺序点


986

什么是“序列点”?

未定义行为与序列点之间有什么关系?

我经常使用诸如的有趣且令人费解的表情a[++i] = i;来使自己感觉更好。为什么我应该停止使用它们?

如果您已阅读本文,请确保访问后续问题未定义行为和重载序列点

(注意:这本来是Stack Overflow的C ++ FAQ的一个条目。如果您想批评以这种形式提供FAQ的想法,那么开始所有这些工作的meta上的发布将是这样做的地方。该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)

Answers:


682

C ++ 98和C ++ 03

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


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

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


什么是序列点?

标准说

在执行序列中某些特定的点(称为顺序点)上,以前评估的所有副作用都应完整,并且以后评估的副作用都不应发生。(第1.9 / 7节)

副作用?有什么副作用?

对表达式的求值会产生某些结果,并且如果执行环境的状态另外发生变化,则可以说表达式(对其求值)会产生一些副作用。

例如:

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

除初始化操作外,y由于++运算符的副作用,get 的值也会更改。

到目前为止,一切都很好。继续到序列点。comp.lang.c作者提供的seq点的替代定义Steve Summit

顺序点是指尘埃沉淀下来的时间点,可以确保到目前为止已经看到的所有副作用都已完成。


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是逗号运算符; in func(a,a++) ,不是逗号运算符,它只是参数a和之间的分隔符a++。因此,在这种情况下,行为是不确定的(如果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的末尾。表达式也可以包含intermediate 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 + 1所有访问中i(在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])与最终存储在i中的值无关(它发生在中i++),因此没有很好的定义方式-无论是出于我们的理解还是编译器的访问是否应该在存储增量值之前或之后进行。因此,行为是不确定的。

例子3:

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

在此处跟踪C ++ 11的答案。


45
*p++ = 4 不是未定义的行为。*p++被解释为*(p++)p++返回p(副本),并将值存储在先前的地址中。为什么要调用UB?很好。
Prasoon Saurav

6
@Mike:AFAIK,您没有可链接到的C ++标准的(合法)副本。
2010年

11
好了,那么您可以链接到ISO的相关订单页面。无论如何,考虑一下,短语“ C ++标准的基础知识”在术语上似乎有点矛盾,因为如果您正在阅读该标准,那么您已经超越了基础级别。也许我们可以列出您需要基本了解的语言中的哪些内容,例如表达式语法,操作顺序以及操作符重载?
Mike DeSimone 2010年

41
我不确定引用标准是教新手的最佳方法
Inverse

6
@Adrian第一个表达式调用UB,因为在最后一个表达式++i和对的赋值之间没有序列点i。第二个表达式不会调用UB,因为表达式i不会更改的值i。在第二个示例中,在调用赋值运算符之前,i++其后是序列点(,)。
Kolyunya

276

这是我之前的回答的后续文章,其中包含与C ++ 11相关的材料。


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


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

是! 这是真的。

序列点已被取代测序前测序后(和未测序不定测序关系在C ++ 11。


“先排序”到底是什么东西?

排序前(第1.9 / 13节)是一个关系:

单个线程执行的评估之间,并得出严格的偏序1

从形式上讲,它意味着给定任何两个求值(请参阅下文), A并且B如果在此之前A进行排序 B,则的执行A 应先于的执行B。如果A之前没有进行测序BB之前没有进行测序A,然后AB未测序 2

评价AB不定测序当任一A之前测序BB之前测序A,但它是未指定的,其3

[注释]
1:严格偏序是一个二元关系 "<"在一组P其是asymmetric,和transitive的,即,对于所有的ab以及cP中,我们有:
........(I)。如果a <b则¬(b <a)(asymmetry);
........(ii)。如果a <b和b <c,则a <c(transitivity)。
2:无序评估的执行可能会重叠
3:不确定顺序的求值不能重叠,但可以先执行。


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

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

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

  • 引发副作用

现在(第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)请随时编辑帖子以更正错别字和其他错误。


3
代替“非对称”,在“反对称”关系之前/之后排序。在文本中应该对此进行更改,以符合稍后给出的部分顺序的定义(也与Wikipedia一致)。
TemplateRex

1
为什么7)最后一个示例中的项目是UB?也许应该f(i = -1, i = 1)吗?
Mikhail 2014年

1
我修复了“先后顺序”关系的描述。这是一个严格的偏序。显然,表达式不能在其本身之前进行排序,因此该关系不能是自反的。因此,它是不对称而不是反对称的。
ThomasMcLeod 2014年

1
5)善良使我大吃一惊。Johannes Schaub的解释并不是很容易理解。尤其是因为我相信即使在++i(在+使用它的操作员之前评估值)中,该标准仍然没有说必须消除其副作用。但事实上,因为它返回一个裁判一个lvaluei它本身,它必须完成的副作用,因为评估必须完成,所以值必须是最新的。这实际上是疯狂的部分。
v.oddou 2015年

“ ISO C ++委员会成员认为序列点的东西很难理解。因此,他们决定将其替换为上述关系,只是为了使措词更清晰,准确性更高。” -您有关于该主张的参考吗?在我看来,新关系更难理解。
MM 2015年

30

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 incremented
i = i++ + i; // the behavior is undefined

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


为什么i = i++ + 1;在c ++ 17中定义了行为,我认为即使“右操作数在左操作数之前排序”,但是对“ i ++”的修改和赋值的副作用未排序,请提供更多详细信息以解释这些问题
杰克X

@jackX我扩展了答案:)。
AlexD

是的,我认为“右操作数先于左操作数先排序”这句话的解释细节更为有用。例如“右操作数先于左操作数先排序”意味着与右操作数相关的值计算和副作用是在左操作数之前排序。就像您一样:-)
杰克X

11

我猜这是发生这种变化的根本原因,这不仅是使旧的解释更清晰的表象:原因是并发。未指定的详细说明顺序只是从几种可能的顺序中选择一种,这与顺序之前和之后完全不同,因为如果没有指定的顺序,则可以进行并发求值:旧规则则不行。例如:

f (a,b)

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


5
但是,我相信,如果“ a”或“ b”中的任何一个包含函数调用,它们都是不确定地排序的,而不是未排序的,也就是说,一个人的所有副作用都必须在该人的任何副作用发生之前其他方面,尽管编译器不必就先走哪一个保持一致。如果这不再成立,则会破坏很多代码,这些代码依赖于不重叠的操作(例如,如果“ a”和“ b”分别设置,使用和删除共享静态状态)。
supercat 2010年

2

C99(ISO/IEC 9899:TC3)这似乎从这个讨论迄今以下steteents是关于及其评价问题作了缺席。

子表达的评估顺序和副作用发生的顺序均未指定。(第6.5页67)

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


2
这个问题被标记为C ++而不是C,这很好,因为C ++ 17中的行为与旧版本中的行为完全不同,并且与C11,C99,C90等中的行为无关。与之相关。总体而言,我建议删除此内容。更重要的是,我们需要找到等效的C问答,并确保C没问题(并特别注意C ++ 17会更改规则-C ++ 11及之前的行为与C ++ 11或多或少相同)在C11,虽然空话描述在C仍然使用“序列点”,而C ++ 11及更高版本不。
乔纳森·莱弗勒
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.