未定义的行为和序列点

新手上路,请多包涵

什么是“序列点”?

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

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

如果您已阅读本文,请务必访问后续问题 Undefined behavior and sequence points reloaded

(注意:这是对 Stack Overflow 的 C++ FAQ 的 一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么 开始这一切的 meta 上的帖子就是这样 做的地方。该问题在 C++ 聊天室 中进行监控,FAQ 想法最初是从那里开始的,因此您的答案很可能会被提出该想法的人阅读。)

原文由 Prasoon Saurav 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 784
2 个回答

C++98 和 C++03

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


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

先决条件C++标准 的基本知识


什么是序列点?

标准说

在称为 序列点 的执行序列中的某些指定点处,之前评估的所有 副作用 都应该是完整的,并且后续评估 的副作用 应该没有发生。 (§1.97)

副作用?什么是副作用?

表达式的求值会产生一些东西,如果此外执行环境的状态发生变化,则表示该表达式(其求值)具有一些副作用。

例如:

 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) (here a , b is a comma operator; in func(a,a++) , is not a comma operator, it’s merely a separator between the arguments aa++ 。因此在这种情况下行为是未定义的(如果 a 被认为是原始类型)
  • 在函数调用(无论函数是否是内联函数)中,在对函数体中的任何表达式或语句( §1.9/17 )执行之前发生的所有函数参数(如果有)的求值之后。

1:注意:完整表达式的评估可以包括对在词法上不属于完整表达式的子表达式的评估。例如,评估默认参数表达式(8.3.6)所涉及的子表达式被认为是在调用函数的表达式中创建的,而不是在定义默认参数的表达式中创建的

2 : 指示的运算符是内置运算符,如第 5 节所述。当这些运算符之一在有效上下文中被重载(第 13 条),从而指定用户定义的运算符函数时,表达式指定函数调用和操作数形成一个参数列表,它们之间没有隐含的序列点。


什么是未定义行为?

该标准将 §1.3.12 部分中的未定义行为定义为

行为,例如在使用错误程序结构或错误数据时可能出现的行为,本国际标准对此 没有要求3

当本国际标准省略对任何明确的行为定义的描述时,也可能会出现未定义的行为。

3:允许的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(伴随诊断信息的发布)。

简而言之,未定义的行为意味着从你的鼻子飞出的守护进程到你的女朋友怀孕, 任何事情都 可能发生。


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

在我开始之前,您必须知道 Undefined Behaviour、Unspecified Behavior 和 Implementation Defined Behavior 之间的区别。

您还必须知道 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


  1. 此外,只能访问先验值以确定要存储的值。

这是什么意思?这意味着如果在完整表达式中写入对象,则在同一表达式中对其的任何和所有访问都 必须直接参与要写入的值的计算

例如,在 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] )与最终存储在 i 中的值无关(这发生在 i++ ),因此没有好的方法来定义——无论是为了我们的理解还是编译器的——访问应该发生在存储增量值之前还是之后。所以行为是不确定的。

示例 3:

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


在此处 跟进 C++11 的答案。

原文由 Prasoon Saurav 发布,翻译遵循 CC BY-SA 4.0 许可协议

C++17 ( N4659 ) 包括一个提案 Refining Expression Evaluation Order for Idiomatic C++ 定义了更严格的表达式求值顺序。

特别是 下面这句话

8.18 赋值和复合赋值运算符

……

在所有情况下,赋值都在左右操作数的值计算之后和赋值表达式的值计算之前进行排序。 右操作数在左操作数之前排序。

连同以下说明

如果与表达式 X 关联的每个值计算和每个副作用都在每个值计算和与表达式 Y 关联的每个副作用之前排序,则表示表达式 X 在表达式 Y 之前排序。

使以前未定义的行为的几种情况有效,包括有问题的情况:

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

原文由 AlexD 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题