提问者:小点点

未定义的行为和序列点


什么是“序列点”?

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

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

如果您已经阅读了本文,请务必访问后续问题未定义的行为和重新加载的序列点。

(注意:这是Stack Overflow的C++FAQ的一个条目。如果你想要批评在这个表单中提供FAQ的想法,那么开始这一切的meta上的帖子将是这样做的地方。这个问题的答案在C++聊天室中被监视,FAQ的想法最初是在那里开始的,所以你的答案很可能被提出这个想法的人阅读。)


共3个答案

匿名用户

这个答案是针对较早版本的C++标准的。 标准的C++11和C++14版本没有正式包含“序列点”; 操作将改为“之前排序”或“未排序”或“不确定排序”。 净效果本质上是一样的,只是术语不同。

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

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

标准上说

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

表达式的求值会产生一些结果,另外,如果执行环境的状态发生变化,则表示表达式(其求值)具有一些副作用。

例如:

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

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

到目前为止还不错。 接下来是序列点。 comp.lang.c作者Steve Summit给出的序列点的交替定义:

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

它们是:

>

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

    示例:

    int a = 5; // ; is a sequence point here
    

    在计算第一个表达式(§1.9/18)2之后,计算下列每个表达式

    • a&&; b(§5.14)
    • aB(§5.15)
    • A? b:c(§5.16)
    • a,b(§5.18)(这里a,b是逗号运算符;在函数(a,a++)中,不是逗号运算符,它只是参数aa++之间的分隔符。因此,在这种情况下行为是未定义的(如果a被认为是基元类型))

    在函数调用时(无论函数是否内联),在函数体中执行任何表达式或语句之前对所有函数参数(如果有)进行求值之后(§1.9/17)。

    1:注意:完整表达式的求值可以包括在词法上不属于完整表达式的子表达式的求值。 例如,计算默认参数表达式(8.3.6)所涉及的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

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

    本标准在§1.3.12节中将未定义的行为定义为

    使用错误程序结构或错误数据时可能出现的行为,本国际标准对此没有规定任何要求3

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

    3:允许的未定义行为范围从完全忽略情况并产生不可预测的结果,到在翻译或程序执行期间以环境特征的记录方式(发出或不发出诊断消息)行为,到终止翻译或执行(发出诊断消息)。

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

    在我开始讨论之前,您必须知道未定义的行为,未指定的行为和实现定义的行为之间的区别。

    您还必须知道单个运算符的操作数和单个表达式的子表达式的求值顺序,以及副作用发生的顺序是未指定的

    例如:

    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中,i的所有访问(在L.H.S和R.H.S中)都直接涉及到要写入的值的计算。 所以没事。

    这条规则有效地将法律表达式限制在访问明显在修改之前的那些表达式上。

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

  • 匿名用户

    这是我之前回答的后续内容,包含C++11相关材料。

    必备条件:关系(数学)的基本知识。

    是的! 这是非常真实的。

    在C++11中,序列点已被排序在前和排序在后(以及不排序和不确定排序)关系所取代。

    排序在(§1.9/13)之前的关系是:

    • 不对称
    • 及物

    并导致严格的部分顺序1

    在形式上,它意味着给定任意两个评估(见下文)ab,如果a的顺序在b之前,则a的执行应先于b的执行。 如果ab之前没有排序,且ba之前没有排序,则ab是不排序的2

    a排序在b之前或b排序在a之前时,评估ab的排序是不确定的,但未指定哪个3

    [注释]
    1:严格偏序是在一个集P上的二元关系,它是非对称,且传递,即对于P中的所有ABC,我们有:
    ......(i)。 如果一个<; b则(b不对称);
    .......(ii)。 如果一个<; b和b<; c然后是a<; c(传递性)。
    2:未排序的计算的执行可能重叠。
    3:未确定顺序的计算不能重叠,但可以先执行其中之一。

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

    >

  • 值计算(包括确定用于glvalue计算的对象的标识和获取先前分配给用于prvalue计算的对象的值)和

    副作用的产生。

    现在(§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);
    } 
    
    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; //未定义的行为
    2. i=++i+i++; //未定义的行为
    3. i=++i+++i; //未定义的行为
    4. i=v[i++]; //未定义的行为
    5. i=v[++i]://定义良好的行为
    6. i=i+++1; //未定义的行为
    7. i=++i+1; //定义明确的行为
    8. ++++i; //定义明确的行为
    9. f(i=-1,i=-1); //未定义的行为(见下文)

    当调用一个函数时(无论该函数是否内联),与任何参数表达式或与指定被调用函数的后缀表达式相关联的每个值计算和副作用在被调用函数体中的每个表达式或语句执行之前被排序。 [注意:与不同参数表达式相关联的值计算和副作用是不排序的。-结束注意]

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

    • 在C++0x
    • 中对一个变量执行多个预增量操作
    • 未排序的值计算

    最后说明:

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

  • 匿名用户

    C++17(N4659)包括一个改进惯用C++表达式求值顺序的建议,定义了更严格的表达式求值顺序。

    特别是以下一句

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

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

    并作出以下澄清

    如果与表达式X相关联的每一个值计算和每一个副作用在与表达式Y相关联的每一个值计算和每一个副作用之前被排序,则表达式X被称为在表达式Y之前被排序。

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

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