什么是"序列点"?
未定义行为和序列点之间的关系是什么?
我经常使用有趣和复杂的表达方式,比如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:注:完整表达式的计算可以包括非词汇子表达式的计算。完整表达式的一部分。例如,在计算默认参数表达式(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的后续答案。好的。好啊。
- 我不明白"进一步"的部分。这不是使postfix++无用吗?例如,在表达式*p++ = 4中,正在访问p的先前值,以确定要存储在p中的值(OK),并确定存储4的位置的地址(Not OK?)但这句习语肯定不是未定义的行为吗?
- *p++ = 4 不是未定义的行为。*p++解释为*(p++)。p++返回p份(副本)和存储在前一个地址的值。为什么会调用ub?非常好。
- @Prason:那么返回一份p的副本不算"访问"p的"优先值"?为什么不?这里的"访问"有技术意义吗?
- @用户168715:好的,让我举个例子。例如i = i + i 。这里有3个i的入口。这里,i是用同一个表达式来写的,i的所有访问(lhs中的一个,rhs中的2个)都直接涉及到计算必须写入的最终值。所以很好。这意味着访问先前的值只是为了确定必须写入什么。
- 迈克:AfAIK,没有任何(合法)拷贝的C++标准可以链接到。
- 那么,你可以链接到ISO的相关订单页面。不管怎样,想想看,"C++标准的基础知识"这个短语似乎有点矛盾,因为如果你读的是标准,你就已经超过了初级水平。也许我们可以列出您需要基本理解的语言中的哪些内容,比如表达式语法、操作顺序,或者运算符重载?
- 我不确定引用标准是教新手的最好方法
- @ Prasoon…这真是太好了…我在一个地方得到所有修订…感谢您创建了这个精彩的主题…我已经把它添加到我的最爱。-)
- @普:有一个矛盾的说法。上面的示例1说明了"访问明显先于修改的对象的法律表达式"。但是示例1中的注释反驳了这一点(除非使用上面的"法律表达式"只是为了满足解析器的要求,而不是暗示它已被定义)。
- 建议:在提到内置逗号运算符的部分附近有一个序列点,指出用于分隔带支撑初始值设定项中函数或元素的参数的逗号不算作"逗号运算符"。
- @prason:为什么是i=(i,++i,++i);//未定义的行为,因为在++i(最右边)和i的赋值之间没有序列点(i被修改了不止一次b/w两个sp)->我不理解的是,在你的评论中,你只是说没有序列点b t w i和++i,因为(因为=没有ORD但是在括号中你说i在两个sp之间被修改。那么,到底是什么情况呢,SP是否存在?,
- @m3taspl0it:第一个序列点是表达式中的第二个逗号运算符,第二个序列点是分号。"i"的值在这两个序列点之间修改了两次。
- @终端:术语"序列点"是指局部而非全局的现象。例如,在语句A=(foo1(),foo2())+(bar1(),bar2())中,有一个序列点意味着foo1()在foo2()之前执行,同样,bar2()之前也有bar1(),但是在任何foo()调用和任何bar()调用之间没有序列点。
- @终端:好的,我们以y = x++ = ++x;为例,据我所知=没有定义任何序列点,但是由于从右到左的关联性,这个x++ = ++x;将首先执行,现在告诉我这个(最后一个表达式)语句中的序列点在哪里,这样我可以说x被修改了两次btw两个cons执行序列点
- @超级卫星:The term"sequence point" refers to a local, rather than global, phenomenon。我知道要点了,但你能详细解释一下吗?还有我最后一个问题,谢谢。
- @Anubis先生:在考虑排序时,我建议您使用许多临时变量来编写语句,每个临时变量最多一次编写,并且只有在编写时才能读取,这样每个子表达式都将一个实变量复制到一个临时变量,将一个临时变量复制到一个实变量,或者在两个或多个临时变量之间执行操作。例如,"a=b+c"变为"t1=b;t2=c;t3=t1+t2;a=t3;"。假设第二个"="应该是"=",则示例将变为"T1=X;X=T1+1;T2=X+1;X=T2;T3=(T1==T2);X=T3;"。读取临时文件必须遵循写入操作,但除此之外,"并行"可能会发生其他事情。
- @Anubis先生:一个序列点意味着从表达到左边的所有副作用必须发生在对右边表达的任何部分进行评估之前。表达式不包含序列点。像"x=x+1;"这样的语句将转换为"t1=x+1;x=t1;",T1的读取保证在写入之后发生。如果您的表达式被更改为使用三个不同的变量(因此是合法的代码),它可以通过许多不同的方式重新排序。还请注意,可能有助于进一步扩展:
- @Anubis先生:像a=(表达式)这样的语句应该被认为是:t1=a;a=boom;t2=(表达式);defuse a;a=t2;右手边a的"direct"引用将替换为t1。任何试图通过"a=boom"和"defuse a"之间的任何其他方式访问a的尝试都是一个错误,就标准而言,该错误可能会炸毁计算机或其50英尺范围内的任何东西。我认为在执行您的示例时,应该非常清楚地知道一个人应该远离您的计算机。
- 为什么i=i++是未定义的行为,而i=i+1是好的??
- @bhavikshah在i = i++中,i++修改序列点之间的i,然后再次将值分配给自身。但是在i = i+1中,只访问i而只读取Previous value,该值加1,修改后只存储在i中。
- 我以为我理解了序列点,因为它与逗号运算符有关,但看起来我错了。i = (i,++i,++i);无效,i = (++i,i++,i)有效使我困惑。你能扩大范围吗?
- @adrian第一个表达式调用ub,因为在最后一个++i和i的赋值之间没有序列点。第二个表达式不调用ub,因为表达式i不会更改i的值。在第二个示例中,在调用赋值运算符之前,i++后跟一个序列点(,)。
- 好吧,那么我说的是,i = (++i, ++i, i)和i = (++i, ++i, i++)是有效的吗?
- @Adrian你的第一个例子,i = (++i, ++i, i)是有效的,它包括评估++i(读/写);然后有一个序列点;评估++i(r/w);序列点;评估i(r);最后将结果分配给i(w)。对i的每一次写入都是通过一个序列点与其他写入隔离的,而对i的每一次读取也都是正常的。在您的第二个例子中,您得到:评估EDOCX1(r/w),序列点,评估EDOCX1(r/w),序列点,评估EDOCX1(r/w),将结果分配给i(w)。现在在最后一个序列点之后有两个写入到i:这违反了§5/4规则1)。
- 这个答案似乎与Prason关于i=++i+1的答案相冲突。一个给我们UB,另一个给我们-它是定义的行为:)请解释
- @prason saurav:一个常见的范例是编写类似于int a, b, c = 0; a = b = c;的代码,但是在将b写入(取c的值)之后,没有序列点在两者之间,b被读取到分配给a。这似乎违反了"2)此外,应该只访问先前的值来确定要存储的值。"您的解释是:"此规则有效地将法律表达式约束到那些访问明显先于修改的对象。"我认为分配链接定义得很好,标准引用的引用是正确的。可能是解释错了?
- 我发现一个表达很难理解,所以我问了一个问题:stackoverflow.com/questions/30614396/what-do-i-i-i-1-1-do。希望它有帮助。
- 总之,不明确的行为意味着你女朋友怀孕了。
- 在++i = 2中,++优先于=。因此,对于i++将被执行,h i=2将被执行。i的最终值为2。现在我看不到这个表达式的任何其他执行路径。因此,它不应该是未指定或未定义的。我确实理解表达式中的i在两个序列点之间被分配了两次,因此它应该具有未指定或未定义的行为。但从逻辑上讲,我只能想到一个执行路径,即行为是固定的。
- 虽然在实践中,UB导致我女朋友怀孕的现象还没有被观察到…:)
- A= I+++J?UB还是定义明确?我认为定义明确。
- @user168715在c11中,"在对运算符结果进行值计算之前,对运算符的操作数的值计算进行排序。"因此,在*p++中,*的序列在p++之后。我认为在C++中几乎是一样的:P
这是我之前的答案,包含C++ 11相关的材料。
先决条件:关系的基础知识(数学)。
在C++ 11中没有序列点是真的吗?对!这是真的。
在C++ 11中,序列点已经被排序之前和排序之后(和未排序和不确定的顺序)替换。
这个"之前的顺序"到底是什么?之前的顺序(§1.9/13)是一种关系,即:
在单个线程执行的计算之间,并引发严格的部分顺序1
形式上是指对A和B进行两次评价(见下文),如果A在B之前排序,那么A的执行应先于B的执行。如果A在B之前没有排序,B在A之前没有排序,那么A和B就没有排序2。
当A在B之前排序或B在A之前排序时,A和B的评估是不确定的,但未指明哪一个。
[notes]1:严格的部分顺序是一个二元关系"<"over a set Pwhich is asymmetric,and transitive,即,for all A,Band cin P,we have that:…(i)。如果aasymmetry)……(ii)。如果atransitivity)。2:未排序的评估的执行可能会重叠。3:不确定顺序的计算不能重叠,但可以先执行其中一个。
在C++ 11的上下文中,"评价"一词的含义是什么?在C++ 11中,表达式(或子表达式)的评价一般包括:
现在(§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.
所以,在未定义的行为和上面提到的事情之间一定有某种关系,对吗?
对!正确的。
在(§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中,x和y的值计算是在(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)不调用未定义的行为。请查看以下答案以获得更详细的解释。
最后注意事项:
如果你在文章中发现任何缺陷,请留下评论。超级用户(rep>20000)请毫不犹豫地编辑帖子,以纠正打字错误和其他错误。
- 顺序前后不是"不对称",而是"反对称"关系。这应该在文本中更改,以符合稍后给出的部分顺序的定义(这也与维基百科一致)。
- 为什么上一个例子中的7)项是UB?也许应该是f(i = -1, i = 1)?
- 下面是一个很好的解释:stackoverflow.com/a/21671069
- 我修正了"之前排序"关系的描述。这是一个严格的部分顺序。显然,表达式不能在其自身之前排序,因此关系不能是自反的。因此它是不对称的,而不是反对称的。
- 5)健康的生活让我神志不清。约翰内斯·朔布的解释并不是很简单。尤其是因为我认为,即使在使用++i运算符的+运算符之前对其值进行了评估,标准仍然没有说明其副作用必须完成。但事实上,由于它返回了对lvalue的引用,即i本身,因此它必须完成评估后的副作用,因此该值必须是最新的。事实上,这是最疯狂的部分。
- "ISO C++委员会成员认为序列点的内容很难理解。所以他们决定用上面提到的关系来代替它,只是为了更清楚的措辞和更精确的表达。"—你有没有得到这一主张的参考?在我看来,新的关系更难理解。
- 我如何生成已定义的+++,但我如何生成ub?
- @M.M:我认为新术语肯定与标准化线程模型的引入有关,而不是为了便于理解。
- @唐:你为什么认为++++++i是UB?
- 里面有一个"巴哈马人"…
- A=I++J是否定义得很好?
- 我很想看到,如果ub来自于右侧的副作用和未排序的读数,则用一个不相关的变量(比如a)替换任务的左侧。示例:除非我弄错了,否则a = ++i + ++i;仍然是ub。
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.
它使以前未定义的行为的几种情况有效,包括有问题的情况:
然而,其他一些类似的情况仍然会导致未定义的行为。
在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兼容编译器并不一定意味着应该开始编写这样的表达式。
我想这一变化有一个根本的原因,让旧的解释更清楚不仅仅是表面上的:这个原因是并发性。未指定的精化顺序仅仅是几个可能的序列顺序中的一个的选择,这与之前和之后的顺序有很大的不同,因为如果没有指定的顺序,就可能同时进行评估:与旧规则不同。例如:
以前是A,然后是B,或者是B,然后是A。现在,A和B可以用交错的指令或者甚至在不同的核心上进行评估。
- 不过,我相信,如果"a"或"b"包含一个函数调用,那么它们是不确定顺序的,而不是未排序的,也就是说,一个函数的所有副作用都必须发生在另一个函数的任何副作用之前,尽管编译器不需要在哪一个函数的前面保持一致。如果不再是这样,它将破坏许多依赖于不重叠操作的代码(例如,如果"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)
- 问题是标记C++而不是C,这是很好的,因为C++ 17中的行为与旧版本中的行为有很大的不同,并且与C11、C99、C90等的行为无关,或者与它的关系很小。总的来说,我建议移除这个。更重要的是,我们需要为C找到等价的Q&A,并确保它是OK(并且注意到C++ 17,特别是改变规则——C++ 11中的行为和以前在C11中的行为或多或少相同),尽管在C中描述它的冗长用法仍然使用"序列点",而C++ 11和后面没有。