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

Undefined behavior and sequence points

什么是"序列点"?

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

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

如果你读过这个,一定要访问后续的问题:未定义的行为和重新加载的序列点。

(注:这意味着是堆栈溢出的C++FAQ的一个条目。如果你想批评在这个表单中提供一个常见问题解答的想法,那么在meta上发布的开始所有这一切的地方就是这样做的地方。这个问题的答案是在C++聊天室中进行监控的,FAQ的想法一开始就出现了,所以你的答案很可能会被那些想出这个想法的人读到。


C++ 98与C++ 03

这个答案是针对旧版本的C++标准。标准的C++ 11和C++ 14版本没有正式包含"序列点";操作是"先排序"或"未排序"或"不确定排序"。净效应本质上是相同的,但术语不同。好的。

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

先决条件:C++标准的基础知识好的。什么是序列点?

标准规定好的。

At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations
shall be complete and no side effects of subsequent evaluations shall have taken place. (§1.9/7)

Ok.

副作用?什么是副作用?

对表达式的评估会产生一些东西,此外,如果执行环境的状态发生了变化,则称表达式(其评估)具有一些副作用。好的。

例如:好的。

1
int x = y++; //where y is also an int

除初始化操作外,由于++运算符的副作用,y的值也会发生更改。好的。

到现在为止,一直都还不错。移动到序列点。由comp.lang.c作者Steve Summit给出的seq点的交替定义:好的。

Sequence point is a point in time at which the dust has settled and all side effects which have been seen so far are guaranteed to be complete.

Ok.

C++标准中列出的公共序列点是什么?

那些是:好的。

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

例子:好的。

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

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

2:所示的运算符是内置运算符,如第5条所述。当这些运算符中的一个在有效上下文中被重载(第13条),从而指定用户定义的运算符函数时,表达式指定一个函数调用,操作数形成一个参数列表,它们之间没有隐含的序列点。好的。什么是未定义的行为?

本标准将§1.3.12节中的未定义行为定义为好的。< Buff行情>

使用错误程序结构或错误数据时可能出现的行为,本国际标准未对其施加要求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。好的。

例如:好的。

1
2
3
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。好的。

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

1
2
3
4
5
6
7
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)

但是下面的表达式是好的:好的。

1
2
3
4
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:好的。

1
std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

例2:好的。

1
a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

由于i的一个访问(a[i]中的一个)与存储在i中的值(该值在i++中发生)无关,因此不允许访问,因此没有好的方法来定义访问是在增量valu之前还是之后进行,无论是为了我们的理解还是编译器的理解。E被存储。所以这种行为是不明确的。好的。

例3:好的。

1
int x = i + i++ ;// Similar to above

这里是C++ 11的后续答案。好的。好啊。


这是我之前的答案,包含C++ 11相关的材料。

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

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

对!这是真的。

在C++ 11中,序列点已经被排序之前和排序之后(和未排序和不确定的顺序)替换。

这个"之前的顺序"到底是什么?

之前的顺序(§1.9/13)是一种关系,即:

  • 不对称
  • 传递的

在单个线程执行的计算之间,并引发严格的部分顺序1

形式上是指对AB进行两次评价(见下文),如果AB之前排序,那么A的执行应先于B的执行。如果AB之前没有排序,BA之前没有排序,那么AB就没有排序2。

AB之前排序或BA之前排序时,AB的评估是不确定的,但未指明哪一个。

[notes]1:严格的部分顺序是一个二元关系"<"over a set Pwhich is asymmetric,and transitive,即,for all ABand cin P,we have that:…(i)。如果aasymmetry)……(ii)。如果atransitivity)。2:未排序的评估的执行可能会重叠。3:不确定顺序的计算不能重叠,但可以先执行其中一个。

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

在C++ 11中,表达式(或子表达式)的评价一般包括:

  • 值计算(包括确定用于GLVALUE评估的对象的标识,并获取以前分配给用于PRVALUE评估的对象的值)和

  • 副作用的开始。

现在(§1.9/14)说:

Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.

  • 简单的例子:

    int x;x = 10;++x;

    ++x有关的数值计算和副作用,在x = 10;的数值计算和副作用后进行排序。

所以,在未定义的行为和上面提到的事情之间一定有某种关系,对吗?

对!正确的。

在(§1.9/15)中,有人提到

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced4.

例如:

1
2
3
4
5
int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
}
  • +运算符的操作数的计算是相对不排序的。
  • <<>>运算符的操作数的评估是相对不排序的。
  • 4:在执行期间多次计算的表达式中对于一个程序,不需要在不同的计算中一致地执行其子表达式的未排序和不确定排序的计算。

    (§1.9/15)
    The value computations of the operands of an
    operator are sequenced before the value computation of the result of the operator.

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

    更重要的是

    (§1.9/15) If a side effect on a scalar object is unsequenced relative to either

    (a) another side effect on the same scalar object

    or

    (b) a value computation using the value of the same scalar object.

    the behaviour is undefined.

    实例:

    1
    2
    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)
  • When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function. [Note: Value computations and side effects associated with different argument expressions are unsequenced. — end note]

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

    • C++0x中变量的多预增量运算
    • 未排序值计算

    最后注意事项:

    如果你在文章中发现任何缺陷,请留下评论。超级用户(rep>20000)请毫不犹豫地编辑帖子,以纠正打字错误和其他错误。


    C++ 17(EDCOX1(0))包含一个求精的C++表达式求值求值顺序它定义了更严格的表达式计算顺序。

    特别是增加了以下句子:

    8.18 Assignment and compound assignment operators:....

    In all cases, the assignment is sequenced after the value
    computation of the right and left operands, and before the value computation of the assignment expression.
    The right operand is sequenced before the left operand.

    它使以前未定义的行为的几种情况有效,包括有问题的情况:

    1
    a[++i] = i;

    然而,其他一些类似的情况仍然会导致未定义的行为。

    N4140中:

    1
    i = i++ + 1; // the behavior is undefined

    但在N4659

    1
    2
    i = i++ + 1; // the value of i is incremented
    i = i++ + i; // the behavior is undefined

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


    我想这一变化有一个根本的原因,让旧的解释更清楚不仅仅是表面上的:这个原因是并发性。未指定的精化顺序仅仅是几个可能的序列顺序中的一个的选择,这与之前和之后的顺序有很大的不同,因为如果没有指定的顺序,就可能同时进行评估:与旧规则不同。例如:

    1
    f (a,b)

    以前是A,然后是B,或者是B,然后是A。现在,A和B可以用交错的指令或者甚至在不同的核心上进行评估。


    在迄今为止似乎没有讨论的C99(ISO/IEC 9899:TC3)中,以下是关于评价顺序的问题。

    [...]the order of evaluation of subexpressions and the order in which
    side effects take place are both unspecified. (Section 6.5 pp 67)

    The order of evaluation of the operands is unspecified. If an attempt
    is made to modify the result of an assignment operator or to access it
    after the next sequence point, the behavior[sic] is undefined.(Section
    6.5.16 pp 91)