首页 文章

未定义的行为和序列点

提问于
浏览
924

什么是“序列点”?

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

我经常使用像_284620这样有趣而复杂的表达方式来让自己感觉更好 . 我为什么要停止使用它们?

如果您已阅读此内容,请务必访问后续问题Undefined behavior and sequence points reloaded .

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

5 回答

  • 17

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

    f (a,b)
    

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

  • 641

    C 98和C 03

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


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

    Pre-requisitesC++ Standard的基本知识


    什么是序列点?

    标准说

    在称为序列点的执行序列中的某些特定点处,先前评估的所有副作用应完整,并且不会发生后续评估的副作用 . (§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:允许的未定义行为范围从完全忽略不完全结果的情况,到翻译或程序执行期间的行为,以文件的特征环境(有或没有发出诊断消息),终止翻译或执行(发布诊断消息) .

    简而言之,未定义的行为意味着从你的鼻子飞到你的女朋友怀孕的守护进程可能发生 anything .


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

    在我进入之前你必须知道Undefined Behaviour, Unspecified Behaviour and Implementation Defined Behaviour之间的差异 .

    你也必须知道 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.
    

    另一个例子here .


    现在 §5/4 中的标准说

    • 1) Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression.

    这是什么意思?

    非正式地,它意味着在两个序列点之间不能多次修改变量 . 在表达式语句中, 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) Furthermore, the prior value shall be accessed only to determine the value to be stored.

    这是什么意思?这意味着如果一个对象被写入一个完整的表达式,则在同一个表达式 must be directly involved in the computation of the value to be written 中对它进行任何和所有访问 .

    例如,在 i = i + 1 中, i (在L.H.S和R.H.S中)的所有访问都是要写入的值的 directly involved in computation . 所以很好 .

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

    例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] 中的那个)的访问之一与最终存储在i中的值无关(在 i++ 中发生),所以's no good way to define--either for our understanding or the compiler' s - 是否应该进行访问在存储增量值之前或之后 . 所以行为是不确定的 .

    例3:

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

    Follow up answer for C++11 here.

  • 260

    C99(ISO/IEC 9899:TC3) 中,到目前为止,这个讨论中似乎没有出现以下关于评估顺序的受访者 .

    [...]子表达式的评估顺序和副作用发生的顺序都是未指定的 . (第6.5节第67页)未指定操作数的评估顺序 . 如果尝试修改赋值运算符的结果或在下一个序列点之后访问它,则行为[sic]未定义 . (第6.5.16节第91页)

  • 11

    This is a follow up to my previous answer and contains C++11 related material. .


    Pre-requisites :关系(数学)的基础知识 .


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

    Yes! 这是真的 .

    Sequence Points 已被C11中的 Sequenced BeforeSequenced After (以及 UnsequencedIndeterminately Sequencedrelations替换 .


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

    Sequenced Before (§1.9/ 13)是一种关系:

    由单个thread执行的评估之间,并诱导 strict partial order 1

    形式上它意味着给出任何两个评估(见下文) AB ,如果 Asequenced before B ,那么 A 的执行应该在执行 B 之前 . 如果在 B 之前未对 A 进行排序,并且在 A 之前未对 B 进行排序,则 ABunsequenced 2 .

    当在 B 之前对 A 进行排序或在 A 之前对 B 进行排序时,评估 ABindeterminately sequenced ,但未指定哪个为3 .

    [注] 1:严格的偏序是在集合P上的二元关系“<”,它是不对称的,并且是传递的,即对于P中的所有a,b和c,我们有:...... ..(一世) . 如果a <b则¬(b <a)(不对称); ........(II) . 如果a <b且b <c则a <c(传递性) . 2:未经测试的评估的执行可能重叠 . 3:不确定顺序的评估不能重叠,但可以先执行 .


    C 11中“评价”一词的含义是什么?

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

    现在(§1.9/ 14)说:

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

    • 琐碎的例子:

    int x; x = 10; ++x;

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


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

    Yes! 对 .

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

    除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估是不确定的4 .

    例如 :

    int main()
    {
         int num = 19 ;
         num = (num << 3) + (num >> 3);
    }
    
    • + 运算符的操作数的评估相对于彼此未进行排序 .

    • <<>> 运算符的操作数的评估相对于彼此没有排序 .

    4:在程序执行期间不止一次评估的表达式中,不需要在不同的评估中一致地执行其子表达式的未序列和不确定顺序的评估 .

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

    这意味着在 x + y 中, xy 的值计算在 (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) 不会调用未定义的行为 . 有关更详细的说明,请查看以下答案 .


    Final Note

    如果您发现帖子中有任何缺陷,请发表评论 . 高级用户(代表> 20000)请不要犹豫,编辑帖子以纠正拼写错误和其他错误 .

  • 0

    C++17N4659 )包含一个提议Refining Expression Evaluation Order for Idiomatic C++,它定义了更严格的表达式评估顺序 .

    特别是,添加了 following sentence

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

相关问题