求值顺序
来自 cppreference.cn
任何表达式的任何部分的求值顺序,包括函数实参的求值顺序,都是未指明的(除下面列出的某些例外情况外)。编译器可以以任何顺序求值操作数及其他子表达式,并且在再次求值同一表达式时可以选择另一顺序。
在 C++ 中没有从左到右或从右到左求值的概念。这不应与运算符的从左到右和从右到左的结合性相混淆:表达式 a() + b() + c() 因为 operator+ 的从左到右结合性而被解析成 (a() + b()) + c(),但在运行时,c() 可能被最先、最后或在 a() 或 b() 之间求值。
运行此代码
可能的输出
b c a c a b
目录 |
[编辑] “ Sequenced before ”规则 (C++11 起)
[编辑] 表达式求值
每个表达式的求值包括:
- 值计算:计算由表达式返回的值。这可能涉及确定对象的标识(泛左值求值,例如表达式返回对某个对象的引用),或读取先前赋给对象的值(纯右值求值,例如表达式返回一个数字或其他值)。
- 副作用的引发:访问(读或写)由 volatile 泛左值所指代的的对象,修改(写入)对象,调用一个库 I/O 函数,或调用一个进行任何这些操作的函数。
[编辑] 定序
Sequenced before(定序于……之前)是同一线程中求值 A
和 B
之间的非对称、可传递的成对关系。
- 若
A
sequenced beforeB
(或者等价地,B
sequenced afterA
),则A
的求值将在B
的求值开始前完成。 - 若
A
不 sequenced beforeB
且B
sequenced beforeA
,则B
的求值将在A
的求值开始前完成。 - 若
A
不 sequenced beforeB
且B
不 sequenced beforeA
,则存在两种可能性:A
和B
的求值是无序的 (unsequenced):它们可以按任意顺序执行并且可能重叠(在单个执行线程中,编译器可以交错构成A
和B
的 CPU 指令)。A
和B
的求值是顺序不确定的 (indeterminately sequenced):它们可以按任意顺序执行但不能重叠:要么A
在B
之前完成,要么B
在A
之前完成。下次求值同一表达式时,顺序可能相反。
如果与表达式 X 相关联的每个值计算和每个副作用都定序于与表达式 Y 相关联的每个值计算和每个副作用之前,则称表达式 X 定序于表达式 Y 之前。
[编辑] 规则
1) 每个完整表达式都定序于下一个完整表达式之前。
2) 任何运算符的操作数的值计算(而非其副作用)都定序于该运算符结果的值计算(而非其副作用)之前。
3) 调用函数 func 时(无论函数是否为内联,以及是否使用显式函数调用语法),下列列表中的每一项都定序于下一项之前:
- 每个实参表达式和指代 func 的后缀表达式
|
(C++26 起) |
- func 函数体中的每个表达式或语句
|
(C++26 起) |
4) 内建后缀自增和后缀自减运算符的值计算定序于其副作用之前。
5) 内建前缀自增和前缀自减运算符的副作用定序于其值计算之前(由于定义为复合赋值的隐式规则)。
9) 在列表初始化中,给定初始化器子句的每个值计算和副作用,都定序于花括号包围的逗号分隔的初始化器列表中,任何在其后的初始化器子句的所有值计算和副作用之前。
10) 一个既不定序于函数外的另一个表达式求值之前也不定序于其后的函数调用(可能是另一个函数调用),相对于该求值是顺序不确定的(程序必须表现得如同构成一个函数调用的 CPU 指令不与构成其他表达式(包括其他函数调用)求值的指令交错,即使函数被内联)。
规则 10 有一个例外:在 std::execution::par_unseq 执行策略下由标准库算法进行的函数调用是无序的,并且可以任意地相互交错。 | (C++17 起) |
13) 在函数调用表达式中,指名函数的表达式定序于每个实参表达式和每个默认实参之前。
14) 在函数调用中,每个形参的初始化的值计算和副作用,相对于任何其他形参的值计算和副作用,都是顺序不确定的。
15) 每个重载的运算符,当使用运算符表示法调用时,遵循它所重载的内建运算符的定序规则。
16) 在下标表达式 E1[E2] 中,E1 定序于 E2 之前。
17) 在成员指针表达式 E1.*E2 或 E1->*E2 中,E1 定序于 E2 之前(除非 E1 的动态类型不包含 E2 所引用的成员)。
18) 在移位运算符表达式 E1 << E2 和 E1 >> E2 中,E1 定序于 E2 之前。
19) 在每个简单赋值表达式 E1 = E2 和每个复合赋值表达式 E1 @= E2 中,E2 定序于 E1 之前。
20) 在带括号的初始化器中的逗号分隔的表达式列表中的每个表达式,其求值如同函数调用(顺序不确定)。 |
(C++17 起) |
[编辑] 未定义行为
在下列情况下,行为是未定义的:
1) 对内存位置的副作用相对于同一内存位置的另一个副作用是无序的。
i = ++i + 2; // well-defined i = i++ + 2; // undefined behavior until C++17 f(i = -2, i = -2); // undefined behavior until C++17 f(++i, ++i); // undefined behavior until C++17, unspecified after C++17 i = ++i + i++; // undefined behavior
2) 对内存位置的副作用相对于使用同一内存位置中任何对象的值进行的值计算是无序的。
cout << i << i++; // undefined behavior until C++17 a[i] = i++; // undefined behavior until C++17 n = ++i + i; // undefined behavior
3) 在内存位置中开始或结束对象的生存期,相对于下列任何操作是无序的:
- 对同一内存位置的副作用
- 使用同一内存位置中任何对象的值进行的值计算
- 开始或结束占用与该内存位置重叠的存储空间的对象的生存期
union U { int x, y; } u; (u.x = 1, 0) + (u.y = 2, 0); // undefined behavior
[编辑] 序列点规则 (C++11 前)
[编辑] C++11 前的定义
表达式的求值可能产生副作用,包括:访问由 volatile 左值所指代的对象,修改对象,调用一个库 I/O 函数,或调用一个进行任何这些操作的函数。
序列点是执行序列中的一个点,在此点所有来自序列中先前求值的副作用都已完成,并且后续求值的副作用都尚未开始。
[编辑] C++11 前的规则
1) 在每个完整表达式的末尾有一个序列点(通常在分号处)。
2) 当调用一个函数时(无论函数是否为内联,也无论是否使用了函数调用语法),在所有函数实参(如果有)求值之后,并且在函数体中任何表达式或语句执行之前,有一个序列点。
4) 在复制一个函数的返回值之后,以及在函数外任何表达式执行之前,有一个序列点。
5) 一旦函数开始执行,在被调用函数完成执行之前,不会求值任何来自调用函数的表达式(函数不能交错执行)。
6) 在求值下列四个表达式中的每一个时,使用内建(非重载)运算符,在表达式 a 求值之后有一个序列点。
a && b a || b a ? b : c a , b
[编辑] C++11 前的未定义行为
在下列情况下,行为是未定义的:
1) 在前一个和下一个序列点之间,内存位置中任何对象的值被表达式的求值修改超过一次。
i = ++i + i++; // undefined behavior i = i++ + 1; // undefined behavior i = ++i + 1; // undefined behavior ++ ++i; // undefined behavior f(++i, ++i); // undefined behavior f(i = -1, i = -1); // undefined behavior
2) 在前一个和下一个序列点之间,对于一个其值被表达式求值修改的对象,以确定要存储的值之外的方式访问其先前的值。
cout << i << i++; // undefined behavior a[i] = i++; // undefined behavior
[编辑] 缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 发布时的行为 | 正确的行为 |
---|---|---|---|
CWG 1885 | C++11 | 函数返回时自动 变量销毁的顺序没有明确说明 |
添加了定序规则 |
CWG 1949 | C++11 | “sequenced after” 在 C++ 标准中被使用但未定义 | 定义为 “sequenced before” 的反义 |
CWG 1953 | C++11 | 涉及内存位置的副作用和值计算 可能相对于在同一内存位置中开始或结束 对象的生存期是无序的 |
在这种情况下,行为是 未定义的 |
CWG 2146 | C++98 | 涉及未定义行为的情况未考虑位域 | 已考虑 |
[编辑] 参考
- C++23 标准 (ISO/IEC 14882:2024)
- 6.9.1 程序执行 [intro.execution]
- 7.6.1.6 自增和自减 [expr.post.incr]
- 7.6.2.8 New [expr.new]
- 7.6.14 逻辑与运算符 [expr.log.and]
- 7.6.15 逻辑或运算符 [expr.log.or]
- 7.6.16 条件运算符 [expr.cond]
- 7.6.19 赋值和复合赋值运算符 [expr.ass]
- 7.6.20 逗号运算符 [expr.comma]
- 9.4.5 列表初始化 [dcl.init.list]
- C++20 标准 (ISO/IEC 14882:2020)
- 6.9.1 程序执行 [intro.execution]
- 7.6.1.5 自增和自减 [expr.post.incr]
- 7.6.2.7 New [expr.new]
- 7.6.14 逻辑与运算符 [expr.log.and]
- 7.6.15 逻辑或运算符 [expr.log.or]
- 7.6.16 条件运算符 [expr.cond]
- 7.6.19 赋值和复合赋值运算符 [expr.ass]
- 7.6.20 逗号运算符 [expr.comma]
- 9.4.4 列表初始化 [dcl.init.list]
- C++17 标准 (ISO/IEC 14882:2017)
- 4.6 程序执行 [intro.execution]
- 8.2.6 自增和自减 [expr.post.incr]
- 8.3.4 New [expr.new]
- 8.14 逻辑与运算符 [expr.log.and]
- 8.15 逻辑或运算符 [expr.log.or]
- 8.16 条件运算符 [expr.cond]
- 8.18 赋值和复合赋值运算符 [expr.ass]
- 8.19 逗号运算符 [expr.comma]
- 11.6.4 列表初始化 [dcl.init.list]
- C++14 标准 (ISO/IEC 14882:2014)
- 1.9 程序执行 [intro.execution]
- 5.2.6 自增和自减 [expr.post.incr]
- 5.3.4 New [expr.new]
- 5.14 逻辑与运算符 [expr.log.and]
- 5.15 逻辑或运算符 [expr.log.or]
- 5.16 条件运算符 [expr.cond]
- 5.17 赋值和复合赋值运算符 [expr.ass]
- 5.18 逗号运算符 [expr.comma]
- 8.5.4 列表初始化 [dcl.init.list]
- C++11 标准 (ISO/IEC 14882:2011)
- 1.9 程序执行 [intro.execution]
- 5.2.6 自增和自减 [expr.post.incr]
- 5.3.4 New [expr.new]
- 5.14 逻辑与运算符 [expr.log.and]
- 5.15 逻辑或运算符 [expr.log.or]
- 5.16 条件运算符 [expr.cond]
- 5.17 赋值和复合赋值运算符 [expr.ass]
- 5.18 逗号运算符 [expr.comma]
- 8.5.4 列表初始化 [dcl.init.list]
[编辑] 参阅
- 运算符优先级,其定义了如何从源代码表示构建表达式。
C 文档 中关于求值顺序
|