C++ 未定义的行为和序列点

C++ 未定义的行为和序列点,c++,undefined-behavior,c++-faq,sequence-points,C++,Undefined Behavior,C++ Faq,Sequence Points,什么是序列点 未定义的行为和序列点之间的关系是什么 我经常使用有趣而复杂的表达方式,比如[++I]=I;,让自己感觉好一点。我为什么要停止使用它们 如果你读过这篇文章,一定要访问后续问题 注意:这意味着是一个进入。如果您想对以这种形式提供常见问题解答的想法提出批评,那么您可以这样做。该问题的答案将在中进行监控,常见问题解答的想法从中开始,因此您的答案很可能会被提出该想法的人阅读。 C++98和C++03 此答案适用于C++版本的旧版本。标准的C++11和C++14版本没有正式包含“序列点”;操作

什么是序列点

未定义的行为和序列点之间的关系是什么

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

如果你读过这篇文章,一定要访问后续问题

注意:这意味着是一个进入。如果您想对以这种形式提供常见问题解答的想法提出批评,那么您可以这样做。该问题的答案将在中进行监控,常见问题解答的想法从中开始,因此您的答案很可能会被提出该想法的人阅读。 C++98和C++03

此答案适用于C++版本的旧版本。标准的C++11和C++14版本没有正式包含“序列点”;操作是“在之前排序”或“未排序”或“不确定排序”。净效果基本相同,但术语不同

免责声明:好的。这个答案有点长。所以在阅读时要有耐心。如果你已经知道这些事情,再读一遍也不会让你发疯

先决条件:基本知识

什么是序列点? 标准上说

在被称为序列点的执行序列中的某些指定点上,先前评估的所有副作用 应完整,且未发生后续评估的副作用。§1.9/7

副作用?什么是副作用? 表达式的求值会产生一些结果,此外,如果执行环境的状态发生变化,则表示表达式及其求值会产生一些副作用

例如:

int x = y++; //where y is also an int
int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.
除了初始化操作外,由于++运算符的副作用,y的值也会发生变化

到目前为止还不错。继续讨论序列点。公司lang.c作者Steve Summit给出的顺序点的替代定义:

序列点是一个时间点,在这个时间点上,尘埃已经沉淀下来,到目前为止已经看到的所有副作用都保证是完全的

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为逗号运算符;在funca中,a++不是逗号运算符,它只是参数a和a++之间的分隔符。因此,如果将a视为基元类型,则在这种情况下行为是未定义的 在函数调用时,无论函数是否为内联函数,在计算所有函数参数(如果有)后 在执行函数体§1.9/17中的任何表达式或语句之前发生

1:注意:完整表达式的求值可以包括非词汇性子表达式的求值 完整表达的一部分。例如,计算默认参数表达式8.3.6所涉及的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

2:所示的操作员是内置操作员,如第5条所述。当其中一个运算符在有效上下文中重载,从而指定用户定义的运算符函数时,表达式指定函数调用,操作数形成参数列表,它们之间没有隐含的序列点

什么是未定义的行为? 本标准将§1.3.12节中未定义的行为定义为:

行为,如使用错误的程序结构或错误的数据时可能出现的行为,本国际标准对此不作要求3

当出现以下情况时,也可能会出现未定义的行为 国际标准省略了对行为的任何明确定义的描述

3:允许的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以具有或具有环境特征的文件化方式进行行为- 发出诊断消息后终止转换或执行,并发出诊断消息

简而言之,未定义的行为意味着任何事情都可能发生,从你鼻子里飞出的守护进程到你女朋友怀孕

未定义的行为和序列点之间的关系是什么? 在我开始之前,你必须知道它们之间的区别

您还必须知道单个运算符的操作数求值顺序和单个表达式的子表达式,以及副作用发生的顺序, 没有具体说明

例如:

int x = y++; //where y is also an int
int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.
另一个例子

现在§5/4中的标准说

1在上一个序列点和下一个序列点之间,标量对象的存储值最多应通过计算表达式修改一次。 这是什么意思

非正式地说,这意味着在两个序列点之间,一个变量不能被修改多次。 在表达式语句中,下一个序列点通常位于终止分号处,上一个序列点位于上一个语句的末尾。表达式也可以包含中间序列点

从上面的句子中,以下表达式调用未定义的行为:

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中,L.H.S和R.H.S中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的一个访问与最终存储在i中的值无关,而该值发生在i++中,因此,对于我们的理解或编译器来说,没有好的方法来定义访问是在存储递增的值之前还是之后进行。因此,这种行为是未定义的

例3:

int x = i + i++ ;// Similar to above
C++11的后续答案

这是我的后续文章,包含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之前排序或B在A之前排序时,评估A和B的排序是不确定的,但未指定是哪个3

[附注]
1:严格的偏序是一个我猜这一变化有一个根本原因,让旧的解释更清楚不仅仅是表面的:原因是并发性。未指定的细化顺序仅仅是选择几个可能的序列顺序中的一个,这与之前和之后的顺序大不相同,因为如果没有指定的顺序,则可以进行并行计算:而旧规则则不然。例如:

f (a,b)
以前不是a接b,就是b接a。现在,a和b可以通过指令交错或甚至在不同的核上进行计算

C++17 N4659包括一份提案 它定义了更严格的表达式求值顺序

特别是下面这句话

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

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

连同以下澄清

如果表达式X的每个 值计算和与表达式X关联的每个副作用在每个值之前排序 计算和与表达式Y相关的每个副作用

使以前未定义的行为的几个案例有效,包括有问题的案例:

a[++i] = i;
然而,其他一些类似的情况仍然会导致未定义的行为

在N4140中:

但在N4659中

当然,使用符合C++17标准的编译器并不一定意味着应该开始编写此类表达式。

在C99ISO/IEC 9899:TC3中,这似乎是本次讨论中尚未涉及的内容。以下是关于求值顺序的测试

[…]子表达式的求值顺序以及 发生的副作用均未说明。第6.5节第67页

未指定操作数的求值顺序。如果一次尝试 用于修改赋值运算符的结果或访问它 在下一个序列点之后,行为[sic]未定义 6.5.16第91页

*p++=4是
不是没有定义的行为*p++被解释为*p++。p++返回pa copy和存储在上一个地址的值。为什么会调用UB?这是完全好的。@迈克:AFAIK,没有合法的C++标准拷贝,你可以链接到。那么,你可以有一个链接到ISO的相关订单页面。不管怎样,想想看,C++标准的基本知识似乎有点矛盾,因为如果你读的是标准,你就已经超过了初级水平。也许我们可以列出你需要基本理解的语言,比如表达式语法,操作顺序,还有可能是运算符重载?我不确定引用标准是最好的教学方法newbies@Adrian第一个表达式调用UB,因为在最后一个++i和分配给i之间没有序列点。第二个表达式不调用UB,因为表达式i不更改i的值。在第二个示例中,在调用赋值运算符之前,i++后面跟着一个序列点。不过,我认为,如果“a”或“b”包含函数调用,则它们的顺序是不确定的,而不是不确定的,也就是说,一方的所有副作用都必须在另一方的任何副作用之前发生,尽管编译器不需要在哪一个先到的问题上保持一致。如果这不再是真的,它将破坏许多依赖于不重叠操作的代码,例如,如果“a”和“b”各自设置、使用和取下一个共享的静态。而不是不对称,前后顺序是反对称关系。这应该在文本中进行更改,以符合后面给出的偏序定义,该定义也与维基百科一致。为什么最后一个示例中的7项是UB?也许应该是fi=-1,i=1?我修正了sequenced-before关系的描述。这是一个好主意。显然,表达式不能在其自身之前排序,因此关系不能是自反的。因此,它是不对称的,而不是反对称的。5良好的自我感觉让我的头脑发昏。Johannes Schaub的解释并不完全直截了当。特别是因为我相信,即使在++I中,在使用它的+运算符之前对其进行值计算,标准仍然没有说它的副作用必须完成。但事实上,因为它返回一个对左值的ref,即i本身,它必须已经完成了副作用,因为必须完成评估,因此该值必须是最新的。这是事实上的疯狂部分。+++ +i如何产生定义但却产生了UB的问题?问题是标记的C++而不是C,这是很好的,因为C++ 17中的行为与旧版本的行为有很大的不同,并且与C11、C99、C90等的行为没有关系,或者与它的关系很小。总的来说,我建议去掉这个。更重要的是,我们需要找到C的等效Q&A,确保它是正确的,并注意到C++17尤其改变了规则——C++11及其之前的行为或多或少与C11中的行为相同,尽管在C中描述它的措辞仍然使用“序列点”,而C++11及其之后则没有;是在c++17中定义的行为,我认为即使右操作数是在左操作数之前排序的,但是对I++的修改和赋值的副作用是不排序的,请给出更多细节来解释these@jackX我扩展了答案:。是的,我认为解释句子的细节右操作数在左操作数之前排序更有用。例如右操作数在左操作数之前排序意味着与右操作数相关的值计算和副作用在左操作数之前排序。正如你所做的:-
i = i++ + 1; // the behavior is undefined
i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined