未定义的行为和顺序点

什么是“序列点”?

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

我经常使用有趣而令人费解的表达式,如a[++i] = i; ,让自己感觉更好。 为什么我应该停止使用它们?

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

(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供常见问题的想法,那么开始所有这些的meta上的贴子将成为这样做的地方。那个问题在C ++聊天室中进行监控,常见问题的想法首先出现在C ++聊天室中,所以你的答案很可能会被那些提出这个想法的人阅读。)


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-points的替换定义:

序列点是尘埃已经沉降的时间点,并且迄今为止所见到的所有副作用都保证完成。


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

那些是:

  • 在完整表达式( §1.9/16 )的评估结束时(完整表达式不是另一个表达式的子表达式)。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定义了未定义的行为

    行为,例如在使用错误的程序结构或错误的数据时可能出现的情况,本标准对此没有要求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
    

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

    例3:

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

    在这里跟进答案。


    这是我以前回答的后续内容,并且包含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之前进行测序,或BA之前进行测序,但评估AB不确定 A ,但未指定哪一个。

    [笔记]
    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中,表达式(或子表达式)的评估通常包括:

  • 值计算 (包括确定用于评估值的对象的身份并获取先前分配给对象用于评估值的值)和

  • 引发副作用

  • 现在(§1.9/ 14)说:

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

  • 简单的例子:

    int x; x = 10; ++x;

    ++x值相关的价值计算和副作用在x = 10;的值计算和副作用之后被排序x = 10;


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

    是! 对。

    在(§1.9/ 15)中已经提到

    除了注明的地方外,对个体运算符和子表达式个体表达式的操作数的评估是不确定的 4。

    例如 :

    int main()
    {
         int num = 19 ;
         num = (num << 3) + (num >> 3);
    } 
    
  • +运算符的操作数的评估是相互不相关的。
  • <<>>操作符的操作数的评估是相互不相关的。
  • 4:在一个程序的执行过程中被评估一次以上的表达, 未测序不定测序其子表达式的评估不需要在不同的评价一致的方式进行。

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

    这意味着在x + yxy的值计算在(x + y)的值计算之前被排序。

    更重要的是

    (§1.9/ 15)如果对标量对象的副作用是相对于任何一个而言都是不确定的

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

    要么

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

    行为是不确定的

    例子:

    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)
  • 当调用一个函数时(函数是否内联),每一个与任何参数表达式相关的值计算和副作用,或者用指定被调用函数的后缀表达式,在每个表达式或者语句执行之前,称为功能。 [注意: 与不同参数表达式相关的值计算和副作用是不确定的 。 - 结束注释]

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

  • 对C ++ 0x中的变量执行多次预增量操作
  • 不确定的价值计算

  • 最后注意

    如果您在该帖子中发现任何缺陷,请发表评论。 高级用户(rep> 20000)请不要犹豫,编辑错误纠正和其他错误的帖子。


    我猜测这个变化有一个根本原因,那就是使旧解释更加清晰,这不仅仅是一种美化:理由是并发。 未详细说明的细化顺序仅仅是选择几种可能的连续顺序中的一种,这与排序前后有很大不同,因为如果没有指定顺序,则可以进行并发评估:旧规则不是这样。 例如在:

    f (a,b)
    

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

    链接地址: http://www.djcxy.com/p/2009.html

    上一篇: Undefined behavior and sequence points

    下一篇: What is The Rule of Three?