什么是序列点,它们与未定义的行为有何关系?

What are sequence points, and how do they relate to undefined behavior?

提问人: 提问时间:11/14/2010 最后编辑:21 revs, 12 users 24%Prasoon Saurav 更新时间:9/29/2023 访问量:126051

问:

什么是“序列点”?

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

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

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


(注意:这是Stack Overflow的C++ FAQ的条目。如果你想批评以这种形式提供常见问题解答的想法,那么在开始这一切的 meta 上的帖子将是这样做的地方。该问题的答案在 C++ 聊天室中受到监控,FAQ 的想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。
C 未定义行为 C++-FAQ 序列点

评论


答:

733赞 29 revs, 16 users 94%Prasoon Saurav #1

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§1.9/16

    例:

    int a = 5; // ; is a sequence point here
    
  • 在对以下每个表达式的计算中,在对第一个表达式的评估之后() 2§1.9/18

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(这里 a , b 是逗号运算符;in 不是逗号运算符,它只是参数和 之间的分隔符。因此,在这种情况下,行为是未定义的(如果被认为是原始类型))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. 在上一个和下一个序列点之间,标量对象的存储值最多应通过表达式的计算进行一次修改。

这是什么意思?

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

在上面的句子中,以下表达式调用了 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)

但以下表达式是可以的:

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

    1. 此外,应仅访问先前的值以确定要存储的值。

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

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

该规则有效地将法律表述限制在修改之前明显存在访问的表述。

示例 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 中的值无关(发生在 中),因此没有很好的方法来定义 - 无论是为了我们的理解还是编译器 - 访问应该在存储递增值之前还是之后进行。所以行为是不确定的。ia[i]i++

示例 3 :

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

C++11 的后续答案在这里

评论

47赞 Prasoon Saurav 11/14/2010
*p++ = 4 不是 Undefined Behaviour 。 被解释为 。 返回(副本)和存储在上一个地址的值。为什么要调用 UB?这完全没问题。*p++*(p++)p++p
7赞 sbi 11/14/2010
@Mike:AFAIK,没有您可以链接到的 C++ 标准的(合法)副本。
12赞 Mike DeSimone 11/15/2010
那么,您可以链接到 ISO 的相关订单页面。无论如何,仔细想想,“C++ 标准的基本知识”这句话在术语上似乎有点矛盾,因为如果你正在阅读标准,你就已经超过了初级水平。也许我们可以列出您需要基本了解的语言中的哪些内容,例如表达式语法、操作顺序,以及运算符重载?
45赞 Inverse 11/15/2010
我不确定引用标准是教新手的最佳方式
7赞 Kolyunya 7/1/2013
@Adrian 第一个表达式调用 UB,因为最后一个表达式和赋值 之间没有序列点。第二个表达式不调用 UB,因为 expression 不会更改 的值。在第二个示例中,在调用赋值运算符之前,后跟一个序列点 ()。++iiiii++,
298赞 16 revs, 13 users 77%Prasoon Saurav #2

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


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


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

是的!这是千真万确的。

在 C++11 中,序列点已替换为 Sequenced Before 和 Sequenced After (以及 UnsequencedIndeterminately Sequenced关系


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

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

在由单个线程执行的计算之间,并产生严格的偏序1

从形式上讲,它意味着给定任意两个评估(见下文),并且,如果排序 之前,则执行 的 应先于 的执行。如果之前没有测序,并且之前没有测序,则 和 是未测序的 2.ABABABABBAAB

当在之前测序或之前测序时,评估和被不确定地测序,但未指定是哪3.ABABBA

[注]
1 : 严格偏序是集合 P 上的二元关系“<”,它是不对称的和传递的,即对于 P 中的所有 abc,我们有:
........(i). 如果 a < b 则 ¬ (b < a) (不对称);
........(ii). 如果 A < B 和 B < C,则 A < C(传递性)。
2:未排序评估的执行可能会重叠
3:不确定的顺序评估不能重叠,但可以先执行。


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

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

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

  • 副作用的开始。

现在 (§1.9/14) 说:

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

  • 简单例子:

    int x; x = 10; ++x;

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


所以未定义的行为和上面提到的事情之间一定有某种关系,对吧?

是的!右。

在(§1.9/15)中提到,

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的计算是无序4.

例如:

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. 算子操作数的计算是相对于彼此而言未排序的。+
  2. 和运算符的操作数的计算是相对于彼此而言未排序的。<<>>

4:在执行过程中多次计算的表达式中 对于一个程序,对其子表达式的未排序不确定的排序评估不需要在不同的评估中一致地执行。

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

这意味着在 和 的值计算之前先排序。x + yxy(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)请不要犹豫,编辑帖子以纠正错别字和其他错误。

评论

4赞 TemplateRex 7/22/2012
之前/之后排序的不是“不对称”,而是“反对称”关系。这应该在文本中进行更改,以符合稍后给出的部分顺序的定义(这也与维基百科一致)。
1赞 Mikhail 3/18/2014
为什么最后一个示例中的 7) 项是 UB?也许应该是?f(i = -1, i = 1)
1赞 ThomasMcLeod 6/25/2014
我修复了“排序之前”关系的描述。这是一个严格的部分命令。显然,表达式不能在自身之前排序,因此关系不能是反身的。因此它是不对称的,而不是反对称的。
1赞 v.oddou 1/9/2015
5)被很好地限制了我,这让我大吃一惊。 约翰内斯·肖布(Johannes Schaub)的解释并不完全简单。特别是因为我相信,即使在(在使用它的操作员之前进行价值评估),标准仍然没有说它的副作用必须完成。但实际上,因为它将 ref 返回给 a 本身,所以它必须已经完成了副作用,因为评估必须完成,因此该值必须是最新的。事实上,这是疯狂的部分。++i+lvaluei
1赞 Don Larynx 8/4/2015
++++i 如何生成定义,但 ++++++i 如何生成 UB?
11赞 Yttrill #3

我猜这种变化有一个根本原因,它不仅仅是表面上的,使旧的解释更清晰:这个原因是并发的。未指定的详细说明顺序只是从几种可能的序列排序中选择一个,这与排序之前和之后有很大不同,因为如果没有指定的排序,则可以并发评估:旧规则则不然。例如:

f (a,b)

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

评论

5赞 supercat 12/10/2010
不过,我相信,如果“a”或“b”包含函数调用,它们将是不确定的序列而不是未序列的,也就是说,一个的所有副作用都必须在另一个副作用之前发生,尽管编译器不需要一致地决定哪一个先发生。如果这不再是真的,它将破坏许多依赖于不重叠操作的代码(例如,如果“a”和“b”各自设置、使用和删除共享静态状态)。
37赞 3 revs, 2 users 98%AlexD #4

C++17 () 包括一个建议:优化惯用 C++ 的表达式求值顺序,它定义了更严格的表达式求值顺序。N4659

特别是以下句子

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

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

连同以下澄清

如果每个表达式 X 在表达式 Y 之前排序,则称为表达式 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 的编译器并不一定意味着应该开始编写此类表达式。

评论

1赞 xmh0511 3/10/2020
为什么在 c++17 中定义了行为,我认为即使“右操作数在左操作数之前排序”,但是“i++”的修改和赋值的副作用是未排序的,请给出更多细节来解释这些i = i++ + 1;
0赞 xmh0511 3/26/2020
是的,我认为句子“右操作数先于左操作数排序”的解释细节更有用,例如“右操作数先于左操作数”意味着与右操作数相关的值计算和副作用先于左操作数。就像你所做的那样:-)
0赞 Tim Randall 11/24/2021
@xmh0511我的理解是,有两种机制可以使 1 的值增加。第一个是后递增运算符,第二个是等于 的值的赋值。我的理解是(从 C++17 开始)后增量在赋值之前排序。i = i++ + 1;ii + 1
0赞 Hans Olsson 11/24/2021
@TimRandall我的理解是,副作用是在评估 lhs 的副作用之前排序的,但不一定在赋值运算符的“副作用”之前排序。不过,标准本来可以写得更清楚。i++
0赞 supercat 4/9/2022
据我所知,gcc 和 clang 都不能可靠地确保在调用之前对 .虽然我不认为标准应该要求在函数调用之前进行计算,但我不明白如何解读标准来说明如果 ,则需要存储值 1 到,而不要求存储函数调用之前保存的值。arr[f()] = x;xf()xi==1arr[i++] = i;arr[2]arr[f()] = x;x
2赞 awiebe #5

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

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

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

评论

3赞 Jonathan Leffler 11/30/2018
这个问题被标记为 C++ 而不是 C,这很好,因为 C++17 中的行为与旧版本中的行为完全不同——并且与 C11、C99、C90 等中的行为无关。或者与它关系不大。总的来说,我建议删除它。更重要的是,我们需要找到 C 的等效问答并确保它是可以的(并注意到 C++17 特别改变了规则——C++11 和之前的行为或多或少与 C11 相同,尽管 C 中描述它的措辞仍然使用“序列点”,而 C++11 及更高版本则没有。
0赞 supercat #6

该标准规定,只有当优化转换不会明显影响任何已定义程序的行为时,才能执行优化转换(如果有的话)。编写序列点规则是为了允许以不跨越序列点的方式对操作进行重新排序,即使这种重新排序的效果可能是可观察到的,方法是将任何可以观察到允许转换效果的操作分类为未定义行为。

这种规则制定方法的一个不幸后果是,它使得程序必须明确地强制执行操作的顺序,即使在无关紧要的情况下也是如此。例如,Java 可以在不使用任何内存屏障的情况下缓存字符串的哈希代码;缺少内存屏障可能会导致线程感知到哈希代码未缓存,即使在另一个线程实际缓存它之后也是如此,从而执行冗余的哈希值计算,但偶尔额外计算的成本通常大大低于在每次访问时添加内存屏障的成本。然而,在 C 语言中,当另一个线程正在修改它时尝试读取缓存的哈希代码字段将产生 Undefine 行为,即使在读取尝试的唯一可能效果是产生旧值(指示哈希代码未缓存)或写入的最后一个值(始终是正确的哈希代码)的平台上也是如此。