未定义的行为和序列点
Posted
技术标签:
【中文标题】未定义的行为和序列点【英文标题】:Undefined behavior and sequence points 【发布时间】:2022-01-12 21:04:37 【问题描述】:什么是“序列点”?
未定义行为与序列点之间的关系是什么?
我经常使用像a[++i] = i;
这样有趣而复杂的表达来让自己感觉更好。我为什么要停止使用它们?
如果您已阅读本文,请务必访问后续问题Undefined behavior and sequence points reloaded。
(注意:这是Stack Overflow's C++ FAQ 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么the posting on meta that started all this 将是这样做的地方。回答该问题在C++ chatroom 中进行监控,FAQ 想法最初是从那里开始的,因此您的回答很可能会被提出该想法的人阅读。)【问题讨论】:
【参考方案1】: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 的编译器并不一定意味着应该开始编写这样的表达式。
【讨论】:
为什么i = i++ + 1;
是c++17中定义的行为,我认为即使“右操作数在左操作数之前排序”,但是“i++”的修改和赋值的副作用未排序,请提供更多详细信息以解释这些
@jackX 我扩展了答案:)。
是的,我认为“右操作数在左操作数之前排序”这句话的解释细节更有用。例如“右操作数在左操作数之前排序”表示值计算和右操作数相关的副作用在左操作数之前排序。和你一样:-)
@xmh0511 我对i = i++ + 1;
的解读是,有两种机制可以将i
的值加1。第一种是后自增运算符,第二种是赋值值等于i + 1
。我的理解是(从 C++17 开始)后增量是在赋值之前排序的。
@TimRandall 我的理解是i++
的副作用在评估 lhs 的副作用之前排序,但不一定在赋值运算符的“副作用”之前。不过,标准本来可以写得更清楚。【参考方案2】:
C++98 和 C++03
此答案适用于旧版本的 C++ 标准。标准的 C++11 和 C++14 版本不正式包含“序列点”;操作是“先排序”或“未排序”或“不确定排序”。最终效果基本相同,但术语不同。
免责声明:好的。这个答案有点长。所以阅读时要有耐心。如果你已经知道这些东西,再读一遍也不会让你发疯。
先决条件:C++ Standard的基本知识
什么是序列点?
标准说
在执行序列中称为序列点的某些指定点,之前评估的所有副作用 应是完整的,并且后续评估不会发生副作用。 (§1.9/7)
副作用?什么是副作用?
表达式的求值会产生一些东西,如果另外执行环境的状态发生变化,则表示该表达式(其求值)有一些副作用。
例如:
int x = y++; //where y is also an int
除了初始化操作之外,y
的值也会因为++
操作符的副作用而改变。
到目前为止一切顺利。继续到序列点。 comp.lang.c作者Steve Summit
给出的seq-points的替代定义:
序列点是尘埃落定的时间点,到目前为止已经看到的所有副作用都保证是完整的。
C++ 标准中列出的常见序列点有哪些?
这些是:
在完整表达式的评估结束时 (§1.9/16
)(完整表达式是不是另一个表达式的子表达式的表达式。)1
例子:
int a = 5; // ; is a sequence point here
在对第一个表达式求值之后对以下每个表达式求值(§1.9/18
)2
a && 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:注意:完整表达式的评估可以包括非词法的子表达式的评估 完整表达的一部分。例如,评估默认参数表达式(8.3.6)所涉及的子表达式被认为是在调用函数的表达式中创建的,而不是定义默认参数的表达式
2 : 指示的运算符是内置运算符,如第 5 节所述。当这些运算符之一在有效上下文中重载(第 13 条)时,指定用户定义的运算符函数,表达式指定一个函数调用,操作数形成一个参数列表,它们之间没有隐含的序列点。
什么是未定义行为?
标准将§1.3.12
部分中的未定义行为定义为
行为,例如在使用错误程序结构或错误数据时可能出现的行为,本国际标准没有要求3。
当这 国际标准省略了对行为的任何明确定义的描述。
3 :允许的未定义行为的范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或有- 发出诊断消息),终止翻译或执行(发出诊断消息)。
简而言之,未定义的行为意味着任何事情都可能发生,从从你的鼻子飞出的守护进程到你的女朋友怀孕。
未定义行为与序列点之间的关系是什么?
在我开始之前,您必须知道Undefined Behaviour, Unspecified Behaviour and Implementation Defined Behaviour 之间的区别。
您还必须知道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.
另一个例子here。
现在§5/4
中的标准说
什么意思?
通俗地说,这意味着在两个序列点之间,一个变量不能被多次修改。
在表达式语句中,next sequence point
通常位于终止分号处,previous sequence point
位于前一条语句的末尾。表达式还可能包含中间 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
的所有访问(在 L.H.S 和 R.H.S 中)直接参与计算要写入的值。所以没关系。
此规则有效地将合法表达限制为那些访问明显在修改之前的表达。
示例 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 的答案here。
【讨论】:
*p++ = 4
不是未定义行为。 *p++
被解释为*(p++)
。 p++
返回 p
(副本)和存储在前一个地址的值。为什么会调用UB?完全没问题。
@Mike:AFAIK,没有您可以链接到的 C++ 标准的(合法)副本。
那么,您可以有一个指向 ISO 相关订单页面的链接。无论如何,想一想,“C++ 标准的基本知识”这个短语似乎有点自相矛盾,因为如果你正在阅读标准,那么你已经过了初级水平。也许我们可以列出您需要基本了解的语言中的哪些内容,例如表达式语法、操作顺序,也许还有运算符重载?
我不确定引用标准是教新手的最佳方式
@Adrian 第一个表达式调用 UB,因为在最后一个 ++i
和对 i
的赋值之间没有序列点。第二个表达式不会调用 UB,因为表达式 i
不会更改 i
的值。在第二个示例中,i++
后跟一个序列点 (,
),然后调用赋值运算符。【参考方案3】:
在C99(ISO/IEC 9899:TC3)
中,到目前为止似乎没有出现在讨论中,以下是关于评估顺序的说明。
[...]子表达式的求值顺序和计算顺序 发生的副作用均未指定。 (第 6.5 节,第 67 页)
未指定操作数的计算顺序。如果尝试 用于修改赋值运算符的结果或访问它 在下一个序列点之后,行为[原文如此]是未定义的。(部分 6.5.16 第 91 页)
【讨论】:
问题被标记为 C++ 而不是 C,这很好,因为 C++17 中的行为与旧版本中的行为完全不同——并且与 C11、C99 中的行为无关, C90等。或者与它关系不大。总的来说,我建议删除它。更重要的是,我们需要为 C 找到等效的 Q&A 并确保它正常(并注意 C++17 尤其改变了规则——C++11 及之前的行为或多或少与在 C11 中,尽管在 C 中描述它的措辞仍然使用“序列点”,而 C++11 及更高版本则不使用。【参考方案4】:这是我的previous answer 的后续内容,包含 C++11 相关材料。。
先决条件:关系(数学)的基本知识。
C++11中真的没有序列点吗?
是的!这是非常正确的。
Sequence Points 已被 Sequenced Before 和 Sequenced After (以及 Unsequenced 和 Indeterminately序列化) relations 在 C++11 中。
这个'Sequenced before'到底是什么?
Sequenced Before(§1.9/13) 是一种关系:
Asymmetric Transitive在单个 thread 执行的评估之间并引发严格的偏序1
正式的意思是给定任何两个评估(见下文)A
和B
,如果A
在B
之前排序,则执行A
的 应在B
的执行之前。如果A
没有在B
之前排序并且B
没有在A
之前排序,那么A
和B
是未排序的 2。
当A
在B
之前排序或B
在A
之前排序时,评估A
和B
是不确定排序,但未指定哪个3.
[注释]
1:严格的偏序是binary relation"<"
在asymmetric
和transitive
的集合上的transitive
,即,对于@987654359中的所有a
、b
和c
@,我们有: ........(i)。如果 a asymmetry);
........(二)。如果 a transitivity)。
2:无序评估的执行可以重叠。
3 : 不确定顺序的评估不能重叠,但可以先执行。
在 C++11 的上下文中,“评估”这个词是什么意思?
在 C++11 中,表达式(或子表达式)的求值通常包括:
值计算(包括为glvalue evaluation 确定对象的身份,并为prvalue evaluation 获取先前分配给对象的值)和
副作用的开始。
现在(§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);
+
运算符的操作数的计算相对于彼此是无序的。
<<
和 >>
运算符的操作数的计算相对于彼此是无序的。
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)请随时编辑帖子以纠正错别字和其他错误。
【讨论】:
不是“不对称”,在之前/之后排序的是“反对称”关系。这应该在文本中进行更改,以符合稍后给出的部分顺序的定义(这也与 Wikipedia 一致)。 为什么最后一个例子中的 7) item 是一个 UB?也许应该是f(i = -1, i = 1)
?
我修复了“sequenced before”关系的描述。这是一个strict partial order。显然,一个表达式不能在它自己之前排序,所以关系不能是自反的。因此它是不对称的而不是反对称的。
5) 身体健康让我大吃一惊。 Johannes Schaub 的解释并不完全容易理解。特别是因为我相信即使在++i
(在使用它的+
运算符之前进行值评估),标准仍然没有说它的副作用必须完成。但事实上,因为它返回一个lvalue
的引用,即i
本身,它必须完成副作用,因为必须完成评估,因此值必须是最新的。事实上,这是最疯狂的部分。
如何++++i产生定义但++++++i产生UB?【参考方案5】:
我猜想这种变化有一个根本原因,让旧的解释更清晰不仅仅是表面上的:这个原因是并发性。未指定的细化顺序只是选择几个可能的串行顺序之一,这与之前和之后的顺序完全不同,因为如果没有指定的顺序,则可以进行并发评估:旧规则不是这样。例如在:
f (a,b)
之前是 a 然后 b,或者 b 然后 a。现在,可以使用交错的指令甚至在不同的内核上评估 a 和 b。
【讨论】:
不过我相信,如果 'a' 或 'b' 包含一个函数调用,它们是不确定排序的而不是未排序的,也就是说,一个的所有副作用都需要发生在其他任何副作用之前,尽管编译器不需要在哪个先发生的问题上保持一致。如果这不再正确,它将破坏大量依赖于操作不重叠的代码(例如,如果“a”和“b”各自设置、使用和删除共享静态状态)。以上是关于未定义的行为和序列点的主要内容,如果未能解决你的问题,请参考以下文章
刨根究底字符编码之十四——UTF-16究竟是怎么编码的(“代理区(Surrogate Zone)”,范围为0xD800~0xDFFF(十进制55296~57343),共2048个码点未定义。UTF8和