什么是 “序列点”?
未定义的行为和序列点之间的关系是什么?
我经常使用像a[++i] = i;
这样有趣而复杂的表达方式a[++i] = i;
,让自己感觉更好。我为什么要停止使用它们?
如果您已阅读此内容,请务必访问后续问题重新加载未定义的行为和序列点 。
(注意:这是Stack Overflow 的 C ++ 常见问题解答的一个条目。如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方。这个问题在C ++ 聊天室中受到监控,其中 FAQ 的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读。)
这个答案适用于旧版本的 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 的交替定义:
序列点是尘埃落定的时间点,到目前为止所见的所有副作用都保证完整。
那些是:
§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++) <code translate="no">,</code>
不是逗号运算符,它只是参数之间的隔板a
和a++
因此行为是在这种情况下,未定义。 (如果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
的标准说
这是什么意思?
非正式地,它意味着在两个序列点之间不能多次修改变量。在表达式语句中, 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
这是什么意思?这意味着如果一个对象被写入一个完整的表达式,那么在同一个表达式中对它的任何和所有访问都必须直接参与计算要写入的值 。
例如,在i = i + 1
, i
所有访问(在 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
是不允许的,因为i
的一个访问( a[i]
中的a[i]
)与最终存储在 i 中的值无关(在i++
发生),所以没有好的方法来定义 - 要么是我们的理解,要么是编译器 - 是否应该在存储增量值之前或之后进行访问。所以行为是不确定的。
例 3:
int x = i + i++ ;// Similar to above
在这里跟进 C ++ 11 的答案。
这是我之前的答案的后续内容,包含 C ++ 11 相关资料。 。
先修课程 :关系(数学)的基础知识。
是!这是非常正确的。
序列点已被 C ++ 11 中的Sequenced Before和Sequenced After (以及Unsequenced和Indeterminately Sequenced ) 关系所取代。
排序之前 (§1.9/ 13)是一种关系,它是:
在由单个线程执行的评估之间并引发严格的部分顺序 1
形式上它意味着给出任何两个评估(见下文) A
和B
,如果A
在 B
之前被排序 ,那么A
的执行应该在 B
的执行之前 。如果A
是不是之前测序B
和B
没有之前测序A
,然后A
和B
的未测序 2。
评价A
和B
当任一被不定测序 A
之前测序B
或B
之前进行测序A
,但它是未指定的,其3。
[笔记]
1:严格的偏序是在集合P
的二元关系 "<"
,它是asymmetric
,并且是transitive
,即对于P
所有a
, b
和c
,我们得到:
........(一世)。如果 a asymmetry
);
........(II)。如果 a transitivity
)。
2: 未经测试的评估的执行可能重叠 。
3: 不确定顺序的评估不能重叠 ,但可以先执行。
在 C ++ 11 中,表达式(或子表达式)的评估通常包括:
值计算 (包括确定glvalue 评估对象的标识并获取先前分配给对象以进行prvalue 评估的值 )和
引发副作用 。
现在(§1.9/ 14)说:
在与要评估的下一个全表达式相关联的每个值计算和副作用之前 ,对与全表达式相关联的每个值计算和副作用进行排序 。
琐碎的例子:
int x;
x = 10;
++x;
在x = 10;
的值计算和副作用之后,对与++x
相关的值计算和副作用进行排序x = 10;
是!对。
在(§1.9/ 15)中已经提到过
除非另有说明,否则对个体操作员的操作数和个别表达式的子表达式的评估是不确定的 4 。
例如 :
int main()
{
int num = 19 ;
num = (num << 3) + (num >> 3);
}
+
运算符的操作数的评估相对于彼此是无序的。 <<
和>>
运算符的操作数的评估相对于彼此是无序的。 4:在一个程序的执行过程中被评估一次以上的表达, 未测序和不定测序其子表达式的评估不需要在不同的评价一致的方式进行。
(§1.9/ 15)运算符操作数的值计算在运算符结果的值计算之前排序。
这意味着在x + y
中, x
和y
的值计算在(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)
不会调用未定义的行为。有关更详细的说明,请查看以下答案。
最后注意 :
如果您发现帖子中有任何缺陷,请发表评论。高级用户(代表 > 20000)请不要犹豫,编辑帖子以纠正拼写错误和其他错误。
C ++ 17 ( N4659
)包括一个提出用于 N4659
C ++ 的 精炼表达评估顺序的提议,它定义了更严格的表达式评估顺序。
特别是,增加了以下句子 :
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 的编译器并不一定意味着应该开始编写这样的表达式。