求值顺序
任何表达式任何部分的求值顺序,包括函数参数的求值顺序都是未指定的(下面列出了一些例外)。编译器可以按任何顺序计算操作数和其他子表达式,并且在再次计算相同表达式时可以选择另一个顺序。
在 C++ 中没有从左到右或从右到左求值的概念。这不要与运算符的从左到右和从右到左结合性混淆:表达式 a() + b() + c() 由于运算符 + 的从左到右结合性而被解析为 (a() + b()) + c(),但在运行时 c() 可以在 a() 或 b() 之前、之后或之间求值。
可能的输出
b c a c a b
内容 |
[编辑] “排序于之前”规则 (自 C++11 起)
[编辑] 表达式的求值
每个表达式的求值包括
- 值计算:计算表达式返回的值。这可能涉及确定对象的标识(左值求值,例如,如果表达式返回对某个对象的引用)或读取先前分配给对象的值(右值求值,例如,如果表达式返回一个数字或其他值)。
- 初始化副作用:访问(读取或写入)由 volatile 左值指定的对象,修改(写入)对象,调用库 I/O 函数或调用执行任何这些操作的函数。
[编辑] 排序
排序于之前是同一线程内求值之间的一种非对称、传递、成对关系。
- 如果 A 排序于 B 之前(或者,等效地,B 排序于 A 之后),则 A 的求值将在 B 的求值开始之前完成。
- 如果 A 不排序于 B 之前,并且 B 排序于 A 之前,则 B 的求值将在 A 的求值开始之前完成。
- 如果 A 不排序于 B 之前,并且 B 不排序于 A 之前,则存在两种可能性
- A 和 B 的求值是未排序的:它们可以按任何顺序执行,并且可以重叠(在单个执行线程内,编译器可以交错构成 A 和 B 的 CPU 指令)。
- A 和 B 的求值是不确定排序的:它们可以按任何顺序执行,但不能重叠:要么 A 在 B 之前完成,要么 B 在 A 之前完成。下次计算相同表达式时,顺序可能会相反。
[编辑] 规则
规则 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
[编辑] 顺序点规则 (直到 C++11)
[编辑] C++11 之前的定义
表达式的求值可能会产生副作用,这些副作用包括:访问由 volatile 左值指定的对象、修改对象、调用库 I/O 函数或调用执行任何这些操作的函数。
顺序点是执行序列中的一个点,在该点,序列中先前求值的所有副作用都已完成,并且后续求值的副作用尚未开始。
[编辑] C++11 之前的规则
1) 每个 完整表达式 的末尾都有一个顺序点(通常在分号处)。
2) 调用函数时(无论函数是否为内联函数,以及是否使用了函数调用语法),在所有函数参数(如果有)的求值之后都有一个顺序点,该顺序点发生在函数体中任何表达式或语句的执行之前。
3) 从函数返回时,在函数调用结果的复制初始化之后以及 return
语句 中 表达式(如果有)末尾处所有临时对象的销毁之前都有一个顺序点。
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++ 标准。
DR | 应用于 | 发布时的行为 | 正确行为 |
---|---|---|---|
CWG 1885 | C++11 | 函数返回时自动变量的销毁顺序不明确 |
添加了排序规则 |
CWG 1949 | C++98 | “sequenced after” 在 C++ 标准中被使用但未定义 | 定义为“sequenced before” 的反义 |
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 文档 关于 求值顺序
|