Undefined behavior and sequence points
Undefined behavior and sequence points
\"序列点\"是什么?与未定义行为有何关系?
我经常使用像a[++i] = i;这样的有趣且复杂的表达式让自己感觉更好。为什么我应该停止使用它们呢?
如果你已经阅读了这个问题,一定要访问后续问题Undefined behavior and sequence points reloaded。
(注:这旨在成为Stack Overflow的C ++ FAQ的一部分。如果您想评论以这种形式提供FAQ的想法,则meta上发布的所有问题都应该这样做。那个问题的答案在C ++ chatroom中得到监控,FAQ的想法首先在那里开始,因此您的答案非常可能会被那些提出这个想法的人阅读。)
这是我的 之前的回答 的跟进,并包含了与C++11有关的材料。.
先决条件 :对关系(数学)有初步的了解。
在C++11中是否真的没有序列点?
是的! 这是非常真实的。
序列点在C++11中已被替换为 先序关系 和 后序关系(以及 未排序 和 不确定排序)关系。
这个“先序关系”的确切意思是什么?
先序关系(§1.9/13)是一种关系,其特点为:
在单个线程执行的评估之间,并引发一个严格的部分顺序1。
严格来说,这意味着给定任何两个评估(见下面的代码A和B),如果A先于B排序,则执行A应先于执行B。如果A没有排序在B之前并且B没有排序在A之前,则A和B是未排序的2。
评估A和B在它们中的任何一个先于另一个排序时,则被不确定地排序,但具体是哪一个则没有指定3。
[注]
1: 严格的部分顺序是指一个对于集合P上的二元关系"<"
,它是反对称关系
和传递关系
的二元关系,即对于P中的所有a
、b
和c
,我们有:
.......(i). 如果a < b,则 ¬ (b < a)(反对称性
);
.......(ii). 如果a < b且b < c,则a < c(传递性
)。
2:未排序的评估的执行可能会重叠。
3:不确定排序的评估不能重叠,但是可以先执行任意一个。
C++11中'evaluation'这个词的意义是什么?
在C++11中,一个表达式(或子表达式)的评估通常包括:
现在 (§1.9/14) 表示:
每个完整表达式相关的值计算和副作用在下一个要评估的完整表达式相关的每个值计算和副作用之前序列化。
-
简单的例子:
int x;
x = 10;
++x;
与
x = 10;
相关联的值计算和副作用之后,在++x
相关联的值计算和副作用之前进行。
那么未定义行为与上述内容之间一定存在某种关系,对吗?
是的! 对。
在(§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; // 未定义行为
i = ++i + i++; // 未定义行为
i = ++i + ++i; // 未定义行为
i = v[i++]; // 未定义行为
i = v[++i]; // 定义良好的行为
i = i++ + 1; // 未定义行为
i = ++i + 1; // 定义良好的行为
++++i; // 定义良好的行为
f(i = -1, i = -1); // 未定义行为(见下文)
在调用函数时(无论函数是否为内联函数),与调用的函数指定后缀表达式相关联的每个参数表达式的值计算和副作用,会在调用函数体中的每个表达式或语句执行之前发生。
[注意:不同参数表达式相关的值计算和副作用是未排序的。-注]
表达式(5)
,(7)
和(8)
不会产生未定义的行为。请查看下面的答案以获得更详细的解释。
最后说明:
如果您在帖子中发现任何缺陷,请留下评论。超级用户(声望> 20000)请毫不犹豫地编辑帖子以纠正错别字和其他错误。
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作者Steve Summit
给出的替代定义:
顺序点是时间点,在此时间点上,所有的灰尘都已经落定,迄今所看到的所有副作用都保证已经完成。
在C++标准中列出的常用顺序点有哪些?
它们是:
-
在完全表达式的评估结束时(
§1.9/16
)(完全表达式是不是另一个表达式的子表达式的表达式。)1例如:
int a = 5; // ; is a sequence point here
-
在评估第一个表达式之后,在每个以下表达式的评估中(
§1.9/18
)2a && b (§5.14)
a || b (§5.15)
a ? b : c (§5.16)
a, b (§5.18)
(这里a,b
是逗号运算符;在func(a,a++)
中,,
不是逗号运算符,它仅仅是用于分隔参数a
和a++
。因此,在这种情况下行为是未定义的(如果a
被视为基本类型)。)
-
在函数调用之后(无论函数是否内联),在函数体中的任何表达式或语句执行之前,执行所有函数参数(如果有)的评估之后(
§1.9/17
)。什么是未定义行为?
标准在第
§1.3.12
节中定义了未定义行为,其中包括可能发生在使用错误的程序构造或错误的数据时的行为,对于这种行为,国际标准不会强制任何要求3。
当国际标准省略对行为的任何明确定义时,也可能预期未定义行为。
3: 不允许的未定义行为范围从完全忽略该情况,导致结果不可预测,到在特定环境下表现出文档化的方式(发出或不发出诊断消息)进行翻译或程序执行,或终止翻译或执行(并发出诊断消息)。
简而言之,未定义行为意味着任何事情都有可能发生,从恶魔从你的鼻子飞出来到你的女朋友怀孕。
未定义行为与序列点之间的关系是什么?
在我深入讨论之前,你必须知道未定义行为、未指定行为和实现定义行为之间的区别(S)。
你还必须知道
单个操作符的操作数和单个表达式的子表达式的求值顺序,以及副作用发生的顺序是未指定的
。例如:
int x = 5, y = 6; int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.
另一个示例在这里。
现在,标准在第
§5/4
节中说明:-
- 在前一个和后一个序列点之间,标量对象的存储值最多只被表达式的求值修改一次。
这意味着什么?
非正式地说,它意味着在两个序列点之间,变量不应被修改超过一次。在表达式语句中,
下一个序列点
通常在终止分号处,上一个序列点
在前一条语句的末尾。表达式还可以包含中间的序列点
。从上面的句子中,以下表达式会调用未定义的行为:
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
此规则有效地约束了合法的表达式必须满足在修改之前,所有对于该对象的访问都在计算过程中。
例如,在 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 的访问(在 a[i] 中)与最终被存储在 i 中的值毫无关系(该值在 i++ 中计算),因此无法良好定义 - 无论是我们的理解还是编译器的理解 - 这个访问应该在递增后还是在递增前进行。因此这个行为是未定义的。
示例3:
int x = i + i++ ;// Similar to above
(在C++11中的后续回答在这里。)
-