未定义的行为和顺序点

什么是“序列点”? express

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

我常用有趣和复杂的表达式,例如a[++i] = i; ,让本身感受好些。 为何我应该中止使用它们? ide

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

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


#1楼

C ++ 98和C ++ 03

此答案适用于C ++标准的较旧版本。 该标准的C ++ 11和C ++ 14版本没有正式包含“序列点”。 操做是“先于”或“未排序”或“不肯定地排序”。 最终效果基本相同,可是术语不一样。 spa


免责声明 :好的。 这个答案有点长。 所以阅读时要有耐心。 若是您已经知道这些事情,那么再次阅读它们不会使您发疯。 线程

先决条件C ++标准的基础知识 翻译


什么是序列点?

标准说 code

在执行序列中某些特定的点(称为顺序点)上 ,之前评估的全部反作用都应完整,而且之后评估的反作用都不该发生。 (第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页)

相关文章
相关标签/搜索