提问人: 提问时间:11/14/2010 最后编辑:21 revs, 12 users 24%Prasoon Saurav 更新时间:9/29/2023 访问量:126051
什么是序列点,它们与未定义的行为有何关系?
What are sequence points, and how do they relate to undefined behavior?
问:
什么是“序列点”?
未定义行为和序列点之间有什么关系?
我经常使用有趣而复杂的表达方式,比如,让自己感觉更好。我为什么要停止使用它们?a[++i] = i;
如果您已阅读本文,请务必访问后续问题未定义的行为和重新加载的序列点。
(注意:这是Stack Overflow的C++ FAQ的条目。如果你想批评以这种形式提供常见问题解答的想法,那么在开始这一切的 meta 上的帖子将是这样做的地方。该问题的答案在 C++ 聊天室中受到监控,FAQ 的想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。
答:
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++)
,
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
-
- 在上一个和下一个序列点之间,标量对象的存储值最多应通过表达式的计算进行一次修改。
这是什么意思?
非正式地,它意味着在两个序列点之间,变量不能被修改多次。
在表达式语句中,the 通常位于终止分号处,而 is 位于前一个语句的末尾。表达式还可以包含中间 .next sequence point
previous sequence point
sequence 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
-
- 此外,应仅访问先前的值以确定要存储的值。
这是什么意思?这意味着,如果在完整表达式中写入对象,则在同一表达式中对它的任何和所有访问都必须直接参与要写入的值的计算。
例如,在所有的访问中(在 LHS 和 RHS 中)都直接参与要写入的值的计算。所以没关系。i = i + 1
i
该规则有效地将法律表述限制在修改之前明显存在访问的表述。
示例 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 中的值无关(发生在 中),因此没有很好的方法来定义 - 无论是为了我们的理解还是编译器 - 访问应该在存储递增值之前还是之后进行。所以行为是不确定的。i
a[i]
i++
示例 3 :
int x = i + i++ ;// Similar to above
C++11 的后续答案在这里。
评论
*p++ = 4
不是 Undefined Behaviour 。 被解释为 。 返回(副本)和存储在上一个地址的值。为什么要调用 UB?这完全没问题。*p++
*(p++)
p++
p
++i
i
i
i
i++
,
这是我之前答案的后续,包含 C++11 相关材料。
先决条件:关系(数学)的基本知识。
C++11 中没有序列点是真的吗?
是的!这是千真万确的。
在 C++11 中,序列点已替换为 Sequenced Before 和 Sequenced After (以及 Unsequenced 和 Indeterminately Sequenced) 关系。
这个“之前排序”的东西到底是什么?
Sequenced Before(§1.9/13) 是一种关系,它是:
在由单个线程执行的计算之间,并产生严格的偏序1
从形式上讲,它意味着给定任意两个评估(见下文),并且,如果排序在 之前,则执行 的 应先于 的执行。如果之前没有测序,并且之前没有测序,则 和 是未测序的 2.A
B
A
B
A
B
A
B
B
A
A
B
当在之前测序或之前测序时,评估和被不确定地测序,但未指定是哪3.A
B
A
B
B
A
[注]
1 : 严格偏序是集合 P 上的二元关系“<”
,它是不对称
的和传递的
,即对于 P
中的所有 a
、b
和 c
,我们有:
........(i). 如果 a < b 则 ¬ (b < a) (不对称
);
........(ii). 如果 A < B 和 B < C,则 A < C(传递性
)。
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);
}
- 算子操作数的计算是相对于彼此而言未排序的。
+
- 和运算符的操作数的计算是相对于彼此而言未排序的。
<<
>>
4:在执行过程中多次计算的表达式中 对于一个程序,对其子表达式的未排序和不确定的排序评估不需要在不同的评估中一致地执行。
(§1.9/15) 操作数的值计算 算子在算子结果的值计算之前先排序。
这意味着在 和 的值计算之前先排序。x + y
x
y
(x + y)
更重要的是
(§1.9/15)如果标量对象的副作用相对于以下任一对象是未排序的
(a) 对同一标量对象的另一个副作用
或
(b) 使用同一标量对象的值进行值计算。
行为是未定义的。
例子:
int i = 5, v[10] = { };
void f(int, int);
i = i++ * ++i; // Undefined Behaviour
i = ++i + i++; // Undefined Behaviour
i = ++i + ++i; // Undefined Behaviour
i = v[i++]; // Undefined Behaviour
i = v[++i]: // Well-defined Behavior
i = i++ + 1; // Undefined Behaviour
i = ++i + 1; // Well-defined Behaviour
++++i; // Well-defined Behaviour
f(i = -1, i = -1); // Undefined Behaviour (see below)
调用函数时(无论函数是否内联),在执行被调用函数主体中的每个表达式或语句之前,都会对与任何参数表达式或指定被调用函数的后缀表达式关联的每个值计算和副作用进行排序。[注:与不同参数表达式相关的值计算和副作用是未排序的。 ]
表达式,并且不调用未定义的行为。查看以下答案以获取更详细的说明。(5)
(7)
(8)
结语:
如果您在帖子中发现任何缺陷,请发表评论。高级用户(代表 >20000)请不要犹豫,编辑帖子以纠正错别字和其他错误。
评论
f(i = -1, i = 1)
++i
+
lvalue
i
我猜这种变化有一个根本原因,它不仅仅是表面上的,使旧的解释更清晰:这个原因是并发的。未指定的详细说明顺序只是从几种可能的序列排序中选择一个,这与排序之前和之后有很大不同,因为如果没有指定的排序,则可以并发评估:旧规则则不然。例如:
f (a,b)
以前要么是 A 然后是 B,要么是 B 然后是 A。现在,a 和 b 可以通过交错的指令甚至在不同的内核上进行评估。
评论
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 的编译器并不一定意味着应该开始编写此类表达式。
评论
i = i++ + 1;
i = i++ + 1;
i
i + 1
i++
arr[f()] = x;
x
f()
x
i==1
arr[i++] = i;
arr[2]
arr[f()] = x;
x
到目前为止,在这次讨论中似乎没有关于评估顺序的以下讨论。C99(ISO/IEC 9899:TC3)
[...]子表达式的计算顺序和顺序 副作用的发生均未指定。(第 6.5 节,第 67 页)
操作数的计算顺序未指定。如果尝试 用于修改赋值运算符的结果或访问它 在下一个序列点之后,行为[原文如此]是未定义的。(部分 6.5.16 第 91 页)
评论
该标准规定,只有当优化转换不会明显影响任何已定义程序的行为时,才能执行优化转换(如果有的话)。编写序列点规则是为了允许以不跨越序列点的方式对操作进行重新排序,即使这种重新排序的效果可能是可观察到的,方法是将任何可以观察到允许转换效果的操作分类为未定义行为。
这种规则制定方法的一个不幸后果是,它使得程序必须明确地强制执行操作的顺序,即使在无关紧要的情况下也是如此。例如,Java 可以在不使用任何内存屏障的情况下缓存字符串的哈希代码;缺少内存屏障可能会导致线程感知到哈希代码未缓存,即使在另一个线程实际缓存它之后也是如此,从而执行冗余的哈希值计算,但偶尔额外计算的成本通常大大低于在每次访问时添加内存屏障的成本。然而,在 C 语言中,当另一个线程正在修改它时尝试读取缓存的哈希代码字段将产生 Undefine 行为,即使在读取尝试的唯一可能效果是产生旧值(指示哈希代码未缓存)或写入的最后一个值(始终是正确的哈希代码)的平台上也是如此。
评论