1044

什么是“序列点”?

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

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

如果您已阅读本文,请务必访问后续问题Undefined behavior and sequence points reloaded

(注意:这是对Stack Overflow 的 C++ FAQ 的一个条目。如果您想批评以这种形式提供 FAQ 的想法,那么开始这一切的 meta 上的帖子就是这样做的地方。该问题在C++ 聊天室中进行监控,FAQ 想法最初是从那里开始的,因此您的答案很可能会被提出该想法的人阅读。)
4

5 回答 5

721

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 作者给出的 seq-points 的替代定义Steve Summit

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


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

那些是:

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

    例子 :

    int a = 5; // ; is a sequence point here
    
  • §1.9/18在对第一个表达式 ( )的求值之后的以下每个表达式的求值

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

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

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


什么是未定义行为?

该标准将部分中的未定义行为定义§1.3.12

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

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

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

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


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

在我开始之前,您必须知道Undefined Behaviour、Unspecified Behavior 和 Implementation Defined Behavior之间的区别。

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

这是什么意思?

非正式地,这意味着在两个序列点之间,一个变量不能被多次修改。在表达式语句中,thenext sequence point通常位于终止分号处,而 theprevious sequence point位于前一条语句的末尾。一个表达式也可能包含 middle 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(the one in a[i]) 的访问之一与最终存储在 i 中的值无关(这发生在 in i++),因此没有好的方法来定义 - 无论是为了我们的理解还是为了编译器的——访问是在存储增量值之前还是之后进行。所以行为是不确定的。

示例 3:

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

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

于 2010-11-14T05:39:00.593 回答
292

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


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


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

是的!这是非常真实的。

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


这个'Sequenced before'到底是什么?

Sequenced Before (§1.9/13)是一个关系,它是:

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

形式上,它意味着给定任何两个评估(见下文) A,并且B如果A之前排序 B,则 的执行A 应先于的执行B。如果A之前 没有 排序B并且之前B没有 排序A, 那么AB没有排序 2 .

评估AB不确定排序的,无论A是在之前排序B还是在之前B排序A,但未指定哪个3

[注意]
1:严格的偏序是在一个集合上的二元关系 ,即, 和, 即对于所有, , 和,我们有:"<"PasymmetrictransitiveabcP
........(i)。如果 a < b 则 ¬ (b < a) ( asymmetry);
........(二)。如果 a < b 且 b < c 则 a < c ( transitivity)。
2:未排序的评估的执行可以重叠
3:不确定顺序的评估不能重叠,但可以先执行。


在 C++11 的上下文中,“评估”这个词是什么意思?

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

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

  • 副作用的开始。

现在(§1.9/14)说:

与完整表达式关联的每个值计算和副作用在与要评估的下一个完整表达式关联的每个值计算和副作用之前排序

  • 简单的例子:

    int x; x = 10; ++x;

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


那么 Undefined Behavior 和上面提到的事情之间肯定有某种关系,对吧?

是的!对。

在(§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)请不要犹豫编辑帖子以纠正拼写错误和其他错误。

于 2010-11-15T11:12:25.303 回答
34

C++17 ( N4659) 包含一个提案Refining Expression Evaluation Order for Idiomatic 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 的编译器并不一定意味着应该开始编写这样的表达式。

于 2017-09-12T08:50:14.223 回答
11

我猜测这种变化有一个根本原因,使旧的解释更清晰不仅仅是表面上的:这个原因是并发性。未指定的细化顺序只是选择几个可能的串行顺序之一,这与之前和之后的顺序完全不同,因为如果没有指定的顺序,则可以进行并发评估:旧规则不是这样。例如在:

f (a,b)

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

于 2010-12-07T19:44:51.267 回答
2

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

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

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

于 2018-10-26T03:40:09.073 回答