未定义的行为和顺序点

生来就可爱ヽ(ⅴ<●) 提交于 2019-12-26 10:41:11

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

什么是“序列点”?

未定义行为与序列点之间有什么关系?

我经常使用有趣和复杂的表达式,例如a[++i] = i; ,让自己感觉好些。 为什么我应该停止使用它们?

如果您已阅读此书,请务必访问后续问题未定义行为和重载序列点

(注意:这本来是Stack Overflow的C ++ FAQ的条目。如果您想批评以这种形式提供FAQ的想法,那么在所有这些开始的meta上的张贴将是这样做的地方。该问题在C ++聊天室中进行监控,该问题最初是从FAQ想法开始的,所以提出这个想法的人很可能会读懂您的答案。)


#1楼

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给出的seq-point的交替定义:

顺序点是粉尘沉淀的时间点,可以保证到目前为止已经看到的所有副作用都是完整的。


C ++标准中列出的常见序列点是什么?

那些是:

  • 在对完整表达式的评估结束时(第§1.9/16 )(完整表达式是不是另一个表达式的子表达式的表达式。) 1

    范例:

    int a = 5; // ; is a sequence point here
  • 在对第一个表达式求值之后对以下每个表达式求值(第§1.9/182

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18) (此处a,b是逗号运算符;在func(a,a++) ,它不是逗号运算符,它只是参数aa++之间的分隔符。因此,这种情况下的行为是不确定的(如果a被认为是原始类型)
  • 在对所有函数参数(如果有的话)进行求值之后(在函数主体中执行任何表达式或语句之前)在函数调用(函数是否为内联)时(第§1.9/17 )。

1:注意:对全表达式的评估可以包括对不属于全表达式的词法部分的子表达式的评估。例如,与评估默认参数表达式(8.3.6)有关的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

2:所指示的运算符是内置运算符,如第5节所述。在有效上下文中,如果其中一个运算符被重载(第13节),从而指定了用户定义的运算符函数,则该表达式指定函数调用,并且操作数形成一个参数列表,它们之间没有隐含的序列点。


什么是未定义行为?

该标准在第§1.3.12§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的标准说

  • 1) 在上一个序列点与下一个序列点之间,标量对象最多应通过表达式的求值修改其存储值。

这是什么意思?

非正式地,它意味着两个序列点之间的变量不得被多次修改。 在表达式语句中, 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 + 1i所有访问(在LHS和RHS中)都直接涉及要写入的值的计算 。 很好。

该规则将法律表达方式有效地限制在那些在修改之前明显可以访问的表达方式。

范例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的答案。


#2楼

这是我之前的回答的后续文章,其中包含与C ++ 11相关的材料。


先决条件 :关系(数学)基础知识。


C ++ 11中没有序列点是真的吗?

是! 这是真的。

在C ++ 11中, 序列点已被之前顺序之后的顺序 (以及未排序不确定地排序关系取代。


“先排序”到底是什么东西?

排序前 (第1.9 / 13节)是一个关系:

单个线程执行的评估之间,并得出严格的偏序 1

正式它意味着给定任意两个评价(见下文)AB ,如果A之前测序 B ,然后执行A 应当先于执行B 。 如果A是不是之前测序BB没有之前测序A ,然后AB未测序 2。

AB之前先测序或BA之前先测序时,评估AB顺序不确定 ,但不确定哪3

[笔记]
1:严格的偏序是在asymmetric且可transitive的集合P二进制关系 "<" ,即对于P所有abc ,我们都有:
........(一世)。如果a <b然后¬(b <a)( asymmetry );
........(ii)。如果a <b和b <c,则a <c( transitivity )。
2: 无序评估的执行可能会重叠
3: 不确定顺序的求值不能重叠 ,但可以先执行。


在C ++ 11的上下文中,“评估”一词的含义是什么?

在C ++ 11中,对表达式(或子表达式)的求值通常包括:

  • 值计算 (包括确定对象的身份以进行glvalue评估和获取先前分配给对象的值以进行prvalue评估 )和

  • 引发副作用

现在(第1.9 / 14节)说:

要评估下一个完整表达式关联的每个值计算和副作用之前,对与一个完整表达式关联的每个值计算和副作用进行排序

  • 琐碎的例子:

    int x; x = 10; ++x;

    x = 10;的值计算和副作用之后,对与++x相关的值计算和副作用进行排序x = 10;


因此,未定义行为与上述事物之间必须存在某种关系,对吗?

是! 对。

在(§1.9/ 15)中提到

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的求值是无序列的 4

例如 :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. +运算符的操作数的求值相对于彼此是无序的。
  2. <<>>运算符的操作数的求值相对于彼此是无序列的。

4:在作为程序的执行期间不止一次计算的表达式, 未测序不定测序其子表达式的评估不需要在不同的评价一致的方式进行。

(第1.9 / 15节)运算符的操作数的值计算在运算符结果的值计算之前排序。

这意味着在x + y ,对xy的值计算在(x + y)的值计算之前进行排序。

更重要的是

(第1.9 / 15节)如果相对于任何一个标量对象的副作用未排序

(a) 对同一标量对象的另一种副作用

要么

(b) 使用相同标量对象的值进行值计算。

行为是不确定的

例子:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

在调用一个函数时(无论该函数是否为内联),与任何参数表达式或指定所调用函数的后缀表达式相关联的每个值计算和副作用都将在执行主体中的每个表达式或语句之前进行排序。称为函数。 [ 注意: 与不同参数表达式关联的值计算和副作用是无序列的 。 — 尾注 ]

表达式(5)(7)(8)不会调用未定义的行为。 请查看以下答案以获得更详细的说明。


最后说明

如果您发现帖子中有任何缺陷,请发表评论。 高级用户(代表> 20000)请随时编辑帖子以更正错别字和其他错误。


#3楼

我猜这是发生这种变化的根本原因,这不仅是使旧的解释更清晰的表象:原因是并发。 未指定的详细说明顺序仅是从几种可能的顺序中选择一种,这与顺序之前和之后都大不相同,因为如果没有指定的顺序,则可以进行并发求值:旧规则则不行。 例如在:

f (a,b)

先前是a然后b,或者b然后a。 现在,可以使用交错的指令甚至在不同的内核上评估a和b。


#4楼

C ++ 17N4659 )包含一项提议, 用于 N4659 C ++的 表达式评估顺序,该提案定义了更严格的表达式评估顺序。

特别是,增加了以下句子

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

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

它使以前未定义的行为的几种情况有效,包括所讨论的一种情况:

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的编译器并不一定意味着应该开始编写这样的表达式。


#5楼

到目前为止,在该讨论中似乎缺少的C99(ISO/IEC 9899:TC3) ,针对评估顺序进行了以下修改。

子表达式的评估顺序和发生副作用的顺序均未指定。 (第6.5页67)

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!