未定义行为与序列点之间有什么关系?
我经常使用像
a[++i] = i;
这样有趣且复杂的表达方式,让自己感觉更好。为什么我应该停止使用它们?如果你已经阅读了这篇文章,请务必访问后续问题未定义行为和序列点重新加载。
(注:这是Stack Overflow的C++ FAQ的一个条目。如果您想批评以这种形式提供FAQ的想法,那么在meta上发布的帖子将是适合这样做的地方。对该问题的回答在C++聊天室中进行监控,FAQ的想法最初就是在那里提出的,所以您的答案很可能会被那些提出这个想法的人看到。)
a[++i] = i;
这样有趣且复杂的表达方式,让自己感觉更好。为什么我应该停止使用它们?本答案适用于旧版C ++标准。 C ++ 11和C ++ 14标准不正式包含“序列点”;操作被分别称为“顺序排列前”、“未顺序排列”或“不确定地顺序排列”。 其净效果基本相同,但术语略有不同。
免责声明:好的,这个答案有点长。所以请耐心阅读。如果你已经知道这些内容,再次阅读它们不会让你疯狂。
先决条件:对C++标准有基本的了解。
什么是序列点?这是对我之前回答的跟进,包含与C++11相关的内容。
先决条件:对关系(数学)有基本的了解。
是的! 这是非常真实的。
序列点在C++11中已被先于序列和后于序列(以及未确定顺序和不可排序)relations所取代。
Sequenced Before(§1.9/13) 是一种关系,它是:
在单个线程执行的评估之间,并引起一个严格偏序1
正式地说,这意味着对于任何两个评估(见下文) A
和 B
,如果 A
在顺序上先于 B
,则应该在执行 B
之前执行 A
。如果 A
不在 B
的顺序之前,且 B
不在 A
的顺序之前,则 A
和 B
是无序的2。
当A
在B
之前或B
在A
之前时,评估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中,表达式(或子表达式)的评估通常包括:
值计算(包括确定glvalue evaluation中对象的标识以及获取先前分配给对象的值prvalue evaluation)和
启动副作用。
现在(§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; // 未定义行为
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)请毫不犹豫地编辑文章以纠正错别字和其他错误。
f(i = -1, i = 1)
吗? - Mikhail++++++i
是未定义行为? - MikeMBC++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标准的编译器并不意味着应该开始编写这样的表达式。
i = i++ + 1;
是定义行为,我认为即使“右操作数在左操作数之前被排序”,但“i ++”的修改和赋值的副作用是未排序的,请提供更多细节来解释这些。 - xmh0511i = i++ + 1;
的理解是有两个机制将 i
的值增加了1。第一个是后置自增运算符,第二个是赋值操作,其值等于 i+1
。我的理解是(截至C++17),后置自增在赋值之前被排序。 - Tim Randalli++
的副作用在评估 lhs 的副作用之前被排序,但不一定在赋值运算符的“副作用”之前。标准可能可以写得更清楚些。 - Hans Olssonarr[f()] = x;
的情况下,都不能可靠地确保对x
的评估先于对f()
的调用。虽然我不认为标准应该要求对x
的评估先于函数调用,但我不明白如何阅读标准以表明如果i==1
,则arr[i++] = i;
将需要将值1存储到arr[2]
,而不需要要求arr[f()] = x;
存储函数调用前x
所持有的值。 - supercatf (a,b)
以前是先执行a再执行b,或者是先执行b再执行a。现在,a和b可以交错执行指令,甚至可以在不同的核心上执行。
*p++ = 4
不是未定义行为。*p++
被解释为*(p++)
。p++
返回p
的副本,并且将值存储在先前的地址中。为什么会引发未定义行为呢?这是完全合理的。 - Prasoon Saurav++i
和对i
赋值之间没有序列点而产生了未定义行为。第二个表达式没有产生未定义行为,因为表达式i
没有改变i
的值。在第二个示例中,i++
在赋值运算符被调用之前被一个序列点(,
)隔开。 - Kolyunya