命名空间
变体
操作

求值顺序

来自 cppreference.com
< cpp‎ | 语言
 
 
C++ 语言
 
 

任何表达式任何部分的求值顺序,包括函数参数的求值顺序都是未指定的(下面列出了一些例外)。编译器可以按任何顺序计算操作数和其他子表达式,并且在再次计算相同表达式时可以选择另一个顺序。

在 C++ 中没有从左到右或从右到左求值的概念。这不要与运算符的从左到右和从右到左结合性混淆:表达式 a() + b() + c() 由于运算符 + 的从左到右结合性而被解析为 (a() + b()) + c(),但在运行时 c() 可以在 a()b() 之前、之后或之间求值。

#include <cstdio>
 
int a() { return std::puts("a"); }
int b() { return std::puts("b"); }
int c() { return std::puts("c"); }
 
void z(int, int, int) {}
 
int main()
{
    z(a(), b(), c());       // all 6 permutations of output are allowed
    return a() + b() + c(); // all 6 permutations of output are allowed
}

可能的输出

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 之前完成。下次计算相同表达式时,顺序可能会相反。

[编辑] 规则

1) 完整表达式的每个值计算和副作用都排序于下一个完整表达式的每个值计算和副作用之前。
2) 任何运算符的操作数的值计算(但不是副作用)排序于运算符结果的值计算(但不是其副作用)之前。
3) 调用函数时(无论函数是否为内联函数,以及是否使用显式函数调用语法),与任何参数表达式或指定被调用函数的后缀表达式关联的每个值计算和副作用都排序于被调用函数体中每个表达式或语句的执行之前。
4) 内置后缀递增和后缀递减运算符的值计算排序于其副作用之前。
5) 内置前缀递增和前缀递减运算符的副作用排序于其值计算之前(由于定义为复合赋值而产生的隐式规则)。
6) 内置逻辑 AND 运算符 &&、内置逻辑 OR 运算符 || 和内置逗号运算符 , 的第一个(左)参数的每个值计算和副作用都排序于第二个(右)参数的每个值计算和副作用之前。
7)条件运算符 ?: 中第一个表达式关联的每个值计算和副作用都排序于与第二个或第三个表达式关联的每个值计算和副作用之前。
8) 内置赋值运算符和所有内置复合赋值运算符的副作用(修改左参数)排序于左右参数的值计算(但不是副作用)之后,并排序于赋值表达式的值计算之前(即在返回对已修改对象的引用之前)。
9)列表初始化 中,给定初始化子句的每个值计算和副作用都在花括号括起来的逗号分隔的初始化器列表中跟随它的任何初始化子句关联的每个值计算和副作用之前进行排序。
10) 未在函数外部的另一个表达式求值(可能是另一个函数调用)之前或之后排序的函数调用相对于该求值是不确定排序的(程序必须表现得 如同 构成函数调用的 CPU 指令未与构成其他表达式求值的指令(包括其他函数调用)交错,即使该函数是内联的)。
规则 10 有一个例外:由在 std::execution::par_unseq 执行策略下执行的标准库算法进行的函数调用是无序的,并且可以彼此任意交错。 (自 C++17 起)
11) 对分配函数 (operator new) 的调用 相对于(直到 C++17)排序在(自 C++17 起) new 表达式 中构造函数参数的求值之前。
12) 从函数返回时,作为对函数调用求值结果的临时对象的复制初始化排序在 return 语句 的操作数末尾处所有临时对象的销毁之前,而这又排序在包含 return 语句的块的局部变量的销毁之前。
13) 在函数调用表达式中,命名函数的表达式排序在每个参数表达式和每个默认参数之前。
14) 在函数调用中,每个参数初始化的值计算和副作用相对于任何其他参数的值计算和副作用是不确定排序的。
15) 每个重载运算符在使用运算符符号调用时都遵守它重载的内置运算符的排序规则。
16) 在下标表达式 E1[E2] 中,E1 的每个值计算和副作用都在 E2 的每个值计算和副作用之前进行排序。
17) 在指向成员的表达式 E1.*E2E1->*E2 中,E1 的每个值计算和副作用都在 E2 的每个值计算和副作用之前进行排序(除非 E1 的动态类型不包含 E2 引用的成员)。
18) 在移位运算符表达式 E1 << E2E1 >> 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 文档 关于 求值顺序