未定义的行为和顺序点

什么是“序列点”?

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

我经常使用有趣和令人费解的expression式,如a[++i] = i; ,让自己感觉更好。 为什么我应该停止使用它们?

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

(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。

C ++ 98和C ++ 03

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


免责声明 :好的。 这个答案有点长。 所以耐心一边阅读。 如果你已经知道这些东西,再读一遍也不会让你发疯。

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


什么是序列点?

标准说

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

副作用? 什么是副作用?

对expression式的评估产生了一些东西,如果执行环境的状态有变化,则expression式(它的评估)有一些副作用。

例如:

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

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

到现在为止还挺好。 转到序列点。 comp.lang.c作者Steve Summit给出的seq-points的replace定义:

序列点是尘埃已经沉降的时间点,并且迄今为止已经看到的所有副作用都被保证完成。


C ++标准中列出的常见顺序点是什么?

那些是:

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

例如:

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

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18) (在func(a,a++)不是逗号运算符,它只是参数aa++之间的分隔符,如果a被认为是基本types,则行为是未定义的)
  • 在执行函数体( §1.9/17 )中的任何expression式或语句之前发生的所有函数参数(如果有的话)之后进行函数调用(函数是否内联)。

1:注意:对一个完整expression式的评估可以包括不是全expression式的词汇部分的子expression式的评估。 例如,涉及评估默认参数expression式(8.3.6)的子expression式被认为是在调用函数的expression式中创build的,而不是用于定义默认参数的expression式

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


什么是未定义行为?

该标准将§1.3.12未定义行为定义为

行为,例如在使用错误的程序结构或错误的数据时可能出现的,本标准对此没有要求3

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

3:允许的未定义的行为范围从完全忽略情况和不可预测的结果,到在翻译或程序执行期间以文档化的方式performance环境特征(伴随或不伴随发布诊断消息),终止翻译或执行(发出诊断信息)。

总之,未定义的行为意味着任何事情都可能发生从守护神从你的鼻子飞到你的女朋友怀孕。


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

在进入之前,您必须知道“ 未定义行为”,“未指定行为”和“实施定义行为”之间的差异。

你还必须知道, 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) 在前一个序列点和下一个序列点之间,一个标量对象应该通过评估一个expression式来最多修改一次标量对象的存储值。

这是什么意思?

非正式地,这意味着在两个序列点之间一个variables不能被修改多次。 在一个expression式语句中, next sequence point通常在终止分号处,而previous sequence point在前一个语句的结尾处。 一个expression式也可以包含中间的sequence points

从上面的句子中,下面的expression式调用Undefined Behavior:

 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) 

但是下面的expression很好:

 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) 此外,只有在确定要存储的值时才能访问先前值。

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

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

这一规则有效地将法律expression限制在修改之前的访问。

例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 

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

例3:

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

在这里跟上答案。

这是我以前的答案的后续行动,并包含C ++ 11相关的材料。


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


C ++ 11中没有序列点吗?

是! 这是非常真实的。

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


究竟是什么“sorting之前”的事情?

sorting (§1.9/ 13) 之前的关系是:

  • 非对称
  • 及物

在单个线程执行的评估之间进行严格的偏序 1

在forms上,这意味着给予任何两个评估(见下文) AB ,如果A B 之前sorting ,那么A的执行应该在 B的执行之前 。 如果AB之前没有被测序, BA之前没有被测序,那么AB就没有被测序2

评估AB不确定的,AB之前被测序或者BA之前被测序时,但是未指定哪一个3

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


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

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

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

  • 引发副作用

现在(§1.9/ 14)说:

每一个与全expression式相关的数值计算和副作用每一个与下一个要被评估的全expression式相关的数值计算和副作用之前sorting

  • 微不足道的例子:

    int x; x = 10; ++x;

    ++x值相关的价值计算和副作用在x = 10;的值计算和副作用之后被sortingx = 10;


所以不确定行为和上述事情之间肯定有一些关系,对吧?

是! 对。

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

除了注意到的地方之外,对单个操作符和个别expression式的操作数的评估是不确定的 4

例如 :

 int main() { int num = 19 ; num = (num << 3) + (num >> 3); } 
  1. +运算符的操作数的评价是相互不相关的。
  2. <<>>操作符的操作数的评估是相互不相关的。

4:在程序执行过程中多次评估的expression式中,对其子expression式的不确定序列不定序评估不需要在不同的评估中一致地执行。

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

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

更重要的是

(§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)

当调用一个函数(函数是否是内联函数)时,在任何参数expression式或者指定被调用函数的后缀expression式的每一个值计算和副作用在执行每个expression式或者语句称为function。 [ 注意: 与不同参数expression式相关的值计算和副作用是不确定的 。 – 结束注意 ]

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

  • 在C ++ 0x中对variables进行多次预增量操作
  • 不确定的价值计算

最后注意

如果你发现任何缺陷的话,请留下评论。 电力用户(rep> 20000)请不要犹豫,编辑纠错和其他错误的职位。

我猜测这个变化有一个根本的原因,那就是旧的解释不仅仅是表面化的:理由是并发的。 未指定的阐述顺序仅仅是select几个可能的连续顺序中的一个,这与sorting前后有很大的不同,因为如果没有指定的sorting,则可以进行并发评估:旧规则不是这样。 例如在:

 f (a,b) 

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

C ++ 17N4659 )包含一个build议, 为定义更严格的expression式评估顺序的N4659 C ++提供expression式评估顺序。

特别是增加了以下句子

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

在任何情况下,赋值都是在左右操作数的值计算之后,赋值expression式的值计算之前进行sorting的。 右操作数在左操作数之前sorting。

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

 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兼容编译器并不一定意味着应该开始编写这样的expression式。