求值顺序
来自 cppreference.cn
任何表达式的任何部分的求值顺序,包含函数实参的求值顺序,是未指明的(以下列出一些例外)。编译器可以以任何顺序求值运算数和其他子表达式,并且在再次求值同一表达式时可以选择另一种顺序。
C++ 中没有从左到右或从右到左求值的概念。这不应与运算符的从左到右和从右到左结合性混淆:表达式 a() + b() + c() 由于 operator+ 的从左到右结合性,被解析为 (a() + b()) + c(),但 c() 可能在运行时先于、后于或介于 a() 或 b() 之间求值
运行此代码
可能的输出
b c a c a b
目录 |
[编辑] “先序于”规则 (C++11 起)
[编辑] 表达式求值
每个表达式的求值包括
- 值计算:计算表达式返回的值。这可能涉及确定对象的标识(左值求值,例如,如果表达式返回对某个对象的引用)或读取先前赋给对象的值(纯右值求值,例如,如果表达式返回数字或某些其他值)。
- 副作用的启动:访问(读取或写入)由 volatile 左值指定的对象,修改(写入)对象,调用库 I/O 函数,或调用执行任何这些操作的函数。
[编辑] 排序
先序于是同一线程内求值 A
和 B
之间的非对称、传递、成对关系。
- 如果
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
之前完成。下次求值同一表达式时,顺序可能相反。
如果与表达式 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++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
CWG 1885 | C++11 | 函数返回时自动变量的析构排序 不明确 |
添加排序规则 |
CWG 1949 | C++11 | 使用了“后序于”,但在 C++ 标准中未定义 | 定义为 “先序于”的逆 |
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 文档 关于 求值顺序
|