提问



什么是序列点?


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


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


如果你已经读过这个,请务必访问后续问题重新加载未定义的行为和序列点。


<子>
(注意:这是Stack Overflow的C ++常见问题解答的一个条目。如果你想批评提供这种形式的常见问题解答的想法,那么发布所有这些的元数据的发布将是这个问题的答案在C ++聊天室中进行监控,其中FAQ的想法首先开始,所以你的答案很可能被那些提出想法的人阅读。)
[99]

最佳参考


C ++ 98和C ++ 03



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





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


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





什么是序列点?



标准说



  在执行序列中的某些指定点(称为序列点),以前评估的所有副作用
  应完整,不得进行后续评估的副作用。 (§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 + 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





在此处跟进回答。

其它参考1


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





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





C ++ 11中是否没有序列点?



是的!这是非常正确的。


序列点已被之前排序之后排序(以及未排序不确定排序取代C ++ 11中的强关系。)[106]





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



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



  • 不对称

  • 传递



由单个线程执行的评估之间并诱导严格的偏序 1
[107] [108] [109]


正式意味着给予任何两个评估(见下文) AB,如果A 之前进行测序B]],然后A 的执行应先于执行B。如果在B之前A未按顺序排序,A之前没有对B进行排序,则AB 未排序 2


评估AB不确定测序A之前的序列BBA测序之前测序]],但未指明 3


<子> [[注释]]
<子>结果
  1:严格的偏序是一组P二元关系 "<"asymmetrictransitive,即所有[[abc P,我们有:

........(i)。如果a< b然后¬(b asymmetry);

  ........(II)。如果a< b和b<然后a< c(transitivity)。

  2:执行未经测试的评估可以重叠。

  3:不确定顺序的评估不能重叠,但可以先执行。
[110] [111] [112]





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



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



  • 值计算(包括确定glvalue评估对象的标识并获取先前分配给对象以进行prvalue评估的值)和

  • 启动副作用



现在(§1.9/14)说:



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




  • 琐碎的例子:


    int x;
     x = 10;
     ++x;


    x = 10;的值计算和副作用之后对与++x相关的值计算和副作用进行排序。






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



是的!是的。


在(§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; // 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)不会调用未定义的行为。有关更详细的说明,请查看以下答案。



  • 对C ++ 0x
  • 中的变量进行多次预增量操作
  • 无序值计算






最后的注释:


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

其它参考2


C ++ 17 (N4659)包含一个提案,用于修饰惯用语C ++的表达式评估顺序
它定义了更严格的表达评估顺序。[117]


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



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

其它参考3


我猜这个改变有一个根本原因,让旧的解释更清楚:这个理由是并发的。不明确的细化顺序只是选择几个可能的连续排序中的一个,这是完全不同的。在排序之前和之后,因为如果没有指定的排序,则可以进行并发评估:旧规则不是这样。例如:


f (a,b)


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