未定义的行为和序列点

Pra*_*rav 970 c++ c++-faq undefined-behavior sequence-points

什么是"序列点"?

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

我经常使用有趣和复杂的表达方式a[++i] = i;,让自己感觉更好.我为什么要停止使用它们?

如果您已阅读此内容,请务必访问后续问题重新加载未定义的行为和序列点.

(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读.)

Pra*_*rav 671

C++ 98和C++ 03

这个答案适用于旧版本的C++标准.该标准的C++ 11和C++ 14版本没有正式包含"序列点"; 操作是'先前排序'或'未排序'或'不确定排序'.净效果基本相同,但术语不同.


免责声明:好的.这个答案有点长.所以在阅读时要有耐心.如果你已经知道这些东西,再次阅读它们不会让你发疯.

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


什么是序列点?

标准说

在称为序列点的执行序列中的某些特定点,先前评估的所有副作用应该是完整的,并且不会发生后续评估的副作用.(§1.9/ 7)

副作用?有什么副作用?

表达式的评估产生一些东西,并且如果另外存在执行环境状态的变化,则表示该表达式(其评估)具有一些副作用.

例如:

int x = y++; //where y is also an int
Run Code Online (Sandbox Code Playgroud)

除初始化操作外,y由于++操作员的副作用,其值也会发生变化.

到现在为止还挺好.继续前进到序列点.comp.lang.c作者给出的seq-points的交替定义Steve Summit:

序列点是尘埃落定的时间点,到目前为止所见的所有副作用都保证完整.


C++标准中列出了哪些常见的序列点?

那些是:

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

示例:

int a = 5; // ; is a sequence point here
Run Code Online (Sandbox Code Playgroud)
  • 在评估第一个表达式(§1.9/18)2之后评估以下每个表达式

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(这里,b是逗号运算符;在func(a,a++) ,不是逗号运算符,它只是参数之间的隔板aa++.因此,该行为是在这种情况下未定义的(如果a被认为是一个基本类型))
  • 在函数体(§1.9/17)中执行任何表达式或语句之前发生的所有函数参数(如果有)的评估之后,在函数调用(函数是否为内联函数)之后.

1:注意:对完整表达式的评估可以包括对词汇表的评估,这些子表达式不是词性表达式的全部表达式.例如,在计算默认参数表达式(8.3.6)时涉及的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

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


什么是未定义的行为?

标准将Section §1.3.12中的Undefined Behavior定义为

行为,例如在使用错误的程序结构或错误数据时可能出现的行为,本国际标准没有规定任何要求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.
Run Code Online (Sandbox Code Playgroud)

另一个例子在这里.


现在的标准§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)
Run Code Online (Sandbox Code Playgroud)

但是下面的表达式很好:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined
Run Code Online (Sandbox Code Playgroud)
  • 2)此外,只能访问先前值以确定要存储的值.

这是什么意思?这意味着如果一个对象被写入一个完整的表达式,那么在同一个表达式中对它的任何和所有访问都必须直接参与计算要写入的值.

例如,在(在LHS和RHS中)的i = i + 1所有访问i直接涉及要写入的值的计算.所以很好.

此规则有效地将法律表达式约束为在修改之前明显存在访问的表达式.

例1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2
Run Code Online (Sandbox Code Playgroud)

例2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc
Run Code Online (Sandbox Code Playgroud)

是不允许的,因为(其中一个)的访问与最终存储在i中的值(发生在i其中a[i])无关i++,因此没有好的方法来定义 - 无论是为了我们的理解还是编译器 - 是否应在存储增量值之前或之后进行访问.所以行为是不确定的.

例3:

int x = i + i++ ;// Similar to above
Run Code Online (Sandbox Code Playgroud)

在这里跟进C++ 11的答案.

  • `*p ++ = 4`不是未定义的行为.`*p ++`被解释为`*(p ++)`.`p ++`返回`p`(副本)和存储在前一个地址的值.为什么会调用UB?这很好. (45认同)
  • 我不确定引用标准是教新手的最佳方式 (40认同)
  • 那么,您可以链接到ISO的相关订单页面.无论如何,考虑到这一点,短语"C++标准的基本知识"似乎有点矛盾,因为如果你正在阅读标准,那么你已经超过了初级水平.也许我们可以列出您需要基本理解的语言中的内容,例如表达式语法,操作顺序以及运算符重载? (10认同)
  • @Mike:AFAIK,您可以链接到的C++标准没有(合法)副本. (6认同)
  • @Adrian第一个表达式调用UB,因为在最后一个`++ i`和对'i`的分配之间没有序列点.第二个表达式不会调用UB,因为表达式"i"不会改变`i`的值.在第二个例子中,在调用赋值运算符之前,`i ++`后跟一个序列点(`,`). (6认同)
  • @Adrian你的第一个例子,`i =(++ i,++ i,i)`,是有效的 - 它包括评估`++ i`(读/写); 然后有一个序列点; 评估`++ i`(r/w); 序列点; 评价`i`(r); 最后将最后一个结果分配给`i`(w).每个对`i`的写入都是通过序列点与其他写入隔离的,并且每次从`i`读取都是正常的.在你的第二个例子中,你得到:评估`++ i`(r/w),序列点,评估`++ i`(r/w),序列点,评估`i ++'(r/w),将结果分配给`i`(w).现在在最后一个序列点之后有两个写入`i`:违反了`§5/ 4`规则1). (5认同)
  • @Prasoon ......这真的很棒......我在一个地方得到了所有的修改...感谢创建这个很棒的主题...我已经把它添加到我的最爱.:-) (3认同)
  • @BhavikShah在`i = i ++`中,`i ++`在序列点之间修改i,然后再将值赋给自己.但是`i = i + 1`,`i`被访问但只能读取`Previous value`并且这个值加1,然后一旦被修改就存储在`i`中. (3认同)
  • "简而言之,不明确的行为意味着你的女朋友怀孕了." 呃,不,不完全. (3认同)
  • @Prasoon:所以返回`p` 的副本不算作“访问”`p` 的“先验值”?为什么不?这里的“访问”有技术意义吗? (2认同)
  • @ user168715:好的,我来举个例子.例如`i = i + i`.这里我们有3次访问`i`.这里`i`将写在同一个表达式中,`i`(LHS中的一个和RHS中的2个)的所有访问都直接**参与计算必须写入的最终值.所以很好.这意味着只访问先前的值以确定必须写入的内容. (2认同)
  • @Terminal:术语"序列点"是指局部而非全局的现象.例如,在语句a =(foo1(),foo2())+(bar1(),bar2())中,有一个序列点暗示foo1()在foo2()之前执行,同样bar1()在bar2()之前,但在任何foo()调用和任何bar()调用之间没有序列点. (2认同)
  • @先生.Anubis:在考虑排序时,我建议你使用大量一次最多写入的临时变量写出语句,并且只有在写入时才会读取,这样每个子表达式都会将一个真实变量复制到temp,复制一个temp到一个真实变量,或在两个或多个临时值之间起作用.例如,"a = b + c"变为"t1 = b; t2 = c; t3 = t1 + t2; a = t3;".你的例子,假设你的第二个"="应该是"==",将成为"t1 = x; x = t1 + 1; t2 = x + 1; x = t2; t3 =(t1 == t2); X = T3;".读取temp必须遵循写入,但除此之外可能"并行"发生. (2认同)
  • 我发现一个表达难以理解,所以我问了一个问题:http://stackoverflow.com/questions/30614396/what-does-iii-1-1-do.希望能帮助到你. (2认同)

Pra*_*rav 273

这是我之前的答案的后续内容,包含C++ 11相关资料..


先修课程:关系(数学)的基础知识.


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

是! 这是非常正确的.

序列点已被C++ 11中的Sequenced BeforeSequenced After(以及UnsequencedIndeterminately Sequenced)关系所取代.


究竟是什么'之前排序'的事情?

排序之前(§1.9/ 13)是一种关系,它是:

在由单个线程执行的评估之间并引发严格的部分顺序1

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

评价AB不定测序或者当A前进行测序BB之前测序A,但它是未指定的,其3.

[注释]
1:严格偏序是一个二元关系 "<"在一组P其是asymmetric,和transitive的,即,对于所有的a,b以及cP中,我们有:
........(I).如果a <b则¬(b <a)(asymmetry);
........(II).如果a <b且b <c则a <c(transitivity).
2:未经测试的评估的执行可能重叠.
3:不确定顺序的评估不能重叠,但可以先执行.


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

在C++ 11中,表达式(或子表达式)的评估通常包括:

现在(§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);
} 
Run Code Online (Sandbox Code Playgroud)
  1. 对操作+员的操作数的评估相对于彼此是不确定的.
  2. 对操作数<<>>操作符的评估相对于彼此是不确定的.

4:在一个程序的执行过程中被评估一次以上的表达,未测序不定测序其子表达式的评估不需要在不同的评价一致的方式进行.

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

这意味着在x + y值计算之前,x并且y在计算值之前对其进行排序(x + y).

更重要的是

(§1.9/ 15)如果标量对象的副作用相对于其中任何一个都没有排序

(a)对同一标量物体的另一个副作用

要么

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

行为未定义.

例子:

int i = 5, v[10] = { };
void  f(int,  int);
Run Code Online (Sandbox Code Playgroud)
  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认同)

Ale*_*exD 26

C++ 17(N4659)包括一个提议精炼表达式评估顺序的Idiomatic C++ ,它定义了更严格的表达式评估顺序.

特别是,增加了以下句子:

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

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

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

a[++i] = i;
Run Code Online (Sandbox Code Playgroud)

然而,其他几个类似的案例仍会导致未定义的行为.

N4140:

i = i++ + 1; // the behavior is undefined
Run Code Online (Sandbox Code Playgroud)

但在 N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined
Run Code Online (Sandbox Code Playgroud)

当然,使用符合C++ 17的编译器并不一定意味着应该开始编写这样的表达式.


Ytt*_*ill 11

我猜这个改变有一个根本原因,让旧的解释更清晰,不仅仅是装饰性的:原因是并发性.未指定的细化顺序仅仅是选择几个可能的连续排序中的一个,这与排序之前和之后完全不同,因为如果没有指定的排序,则可以进行并发评估:旧规则不是这样.例如:

f (a,b)
Run Code Online (Sandbox Code Playgroud)

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

  • 但我相信,如果'a'或'b'包含一个函数调用,它们是不确定的序列而不是未序列的,也就是说,所有副作用都需要在来自一个副作用之前发生.另外,虽然编译器不需要对哪一个先行一致.如果这不再是真的,它将破坏许多代码,这些代码依赖于不重叠的操作(例如,如果'a'和'b'分别设置,使用和取下,则为共享静态状态). (5认同)

归档时间:

查看次数:

101050 次

最近记录:

6 年,2 月 前