求值的顺序

来自cppreference.com
< cpp‎ | language
 
 
 
表达式
概述
值类别(左值 lvalue、右值 rvalue、亡值 xvalue)
求值顺序(序列点)
常量表达式
不求值表达式
初等表达式
lambda 表达式(C++11)
字面量
整数字面量
浮点字面量
布尔字面量
字符字面量,包含转义序列
字符串字面量
空指针字面量(C++11)
用户定义字面量(C++11)
运算符
赋值运算符a=ba+=ba-=ba*=ba/=ba%=ba&=ba|=ba^=ba<<=ba>>=b
自增与自减++a--aa++a--
算术运算符+a-aa+ba-ba*ba/ba%b~aa&ba|ba^ba<<ba>>b
逻辑运算符a||ba&&b!a
比较运算符a==ba!=ba<ba>ba<=ba>=b
成员访问运算符a[b]*a&aa->ba.ba->*ba.*b
其他运算符a(...)a,ba?b:c
运算符的替代表示形式
优先级和结合性
折叠表达式(C++17)
new 表达式
delete 表达式
throw 表达式
alignof
sizeof
sizeof...(C++11)
typeid
noexcept(C++11)
运算符重载
类型转换
隐式转换
const_cast
static_cast
reinterpret_cast
dynamic_cast
显式转换 (T)aT(a)
用户定义转换
 

对几乎所有 C++ 运算符的操作数进行求值的顺序(也包括函数调用表达式中的各函数实参的求值顺序,以及任何表达式中各子表达式的求值顺序)都是未指明的。编译器可以按任何顺序对操作数进行求值,也可以在多次求值同一个表达式时选择不同的顺序。

下文中列出了这条规则的例外情况。

除了下文注明的情况外,C++ 中并没有“从左向右”或“从右向左”求值的概念。请勿将之与运算符的“从左向右”或“从右向左”的结合性相混淆:表达式 f1() + f2() + f3() 根据 operator+ 的从左向右的结合性被解析为 (f1() + f2()) + f3(),但在运行时,对 f3 的函数调用的求值可能首先进行,最后进行,或者在 f1()f2() 之间进行。

目录

[编辑] “按顺序早于”规则(C++11 起)

[编辑] 定义

[编辑] 求值

编译器为每个表达式或子表达式实施两种求值(二者均为可选):

  • 值计算:计算表达式所返回的值。这将涉及对对象的标识(泛左值求值,例如当表达式返回某个对象的引用时),或者读取之前赋值给某个对象的值(纯右值求值,例如当表达式返回一个数值或其他的值时)
  • 副作用:访问(读取或写入)由 volatile 泛左值所代表的对象,修改(写入)某个对象,调用某个程序库 I/O 函数,或者调用某个做出了任何这些操作的函数。

[编辑] 顺序

“按顺序早于(sequenced-before)”是同一个线程中的求值之间的一种非对称的,可传递的对偶关系。

  • 如果 A 按顺序早于 B,则 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)列表初始化之中,给定的初始化式子句之中的每个值计算和副作用,均按顺序早于与在花括号之中的逗号分隔的初始化式列表中跟在它后面的任何初始化式子句相关联的每个值计算和副作用。
11) 如果某个函数调用并未按顺序早于按顺序迟于另一个函数调用,则它是顺序不确定的(程序必须表现为如同构成不同函数调用的 CPU 指令之间不会发生交错,即便函数被内联也应如此)。
规则 11 有一种例外情况:在 std::par_unseq 执行策略之下执行的标准库算法所作出的函数调用是无顺序的,并可以任意交错执行。 (C++17 起)
12)new 表达式中,对分配函数(operator new)的调用按顺序早于 (C++17 起)构造函数的各实参的求值是顺序不确定的 (C++17 前)
13) 当从函数返回时,对作为函数调用的求值结果的临时对象所进行的复制初始化,按顺序早于返回语句的操作数的末尾处对全部临时对象所进行的销毁,而后者按顺序早于对包含该返回语句的块中的局部变量所进行的销毁。
(C++14 起)
14) 在函数调用表达式中,指名函数的表达式按顺序早于每个实参表达式和每个默认实参。
15) 在函数调用中,对每个形参所进行的初始化的值计算和副作用,与任何其他形参的值计算和副作用之间是顺序不确定的。
16) 当使用运算符表示形式来调用时,每个重载运算符都遵循它所重载的内建运算符的顺序规则。
17) 在下标表达式 E1[E2] 中,E1 的每个值计算和副作用均按顺序早于 E2 的每个值计算和副作用。
18) 在成员指针表达式 E1.*E2E1->*E2 中,E1 的每个值计算和副作用均按顺序早于 E2 的每个值计算和副作用(除非 E1 的动态类型并不包含 E2 所指代的成员)。
19) 在位移运算符表达式 E1<<E2E1>>E2 中,E1 的每个值计算和副作用均按顺序早于 E2 的每个值计算和副作用。
20) 在每个简单复制表达式 E1=E2 和每个复合赋值表达式 E1@=E2 中,E2 的每个值计算和副作用均按顺序早于 E1 的每个值计算和副作用。
21) 带括号的初始化式中的表达式逗号分隔列表中的每个表达式,其求值如同函数调用一样(顺序不确定的)。
(C++17 起)

[编辑] 未定义的行为

1) 如果在某个标量对象上的副作用相对于同一个标量对象上的另一个副作用是无顺序的,则其行为是未定义的

i = ++i + 2;       // C++11 前为未定义的行为
i = i++ + 2;       // C++17 前为未定义的行为
f(i = -2, i = -2); // C++17 前为未定义的行为
f(++i, ++i);       // C++17 前为未定义的行为,C++17 后为未指明的行为
i = ++i + i++;     // 未定义的行为

2) 如果在某个标量对象上的副作用相对于使用同一个标量对象的值的一个值计算是无顺序的,则其行为是未定义的

cout << i << i++; // C++17 前为未定义的行为
a[i] = i++;       // C++17 前为未定义的行为
n = ++i + i;      // 未定义的行为

[编辑] 序列点规则 (C++11 前)

[编辑] 定义

表达式的求值可能产生副作用,包括:访问由 volatile 左值所代表的对象,修改某个对象,调用程序库的 I/O 函数,或者调用做出了任何这些操作的函数。

序列点(sequence point)是执行序列中的点,序列中前面的求值的所有副作用已经完成,而其后的任何求值的副作用都未开始。

[编辑] 规则

1) 每个全表达式的末尾(通常在分号处)都是一个序列点。

2) 当调用某个函数时(无论函数是否内联,且无论是否使用了函数调用语法),在所有函数实参(如果有)的求值之后为一个序列点,它发生于对函数体中的任何表达式或语句的求值之前。

3) 在对函数的返回值进行复制之后,并在执行该函数之外的任何表达式之前,为一个序列点。

4) 一旦函数的执行开始,就不再对调用方函数的任何表达式求值,直到被调用函数的执行完成(函数不会交错执行)。

5) 以下四个表达式中的每个的求值,当使用内建的(非重载的)运算符时,在表达式 a 的求值之后均有一个序列点。

a && b
a || b
a ? b : c
a , b

[编辑] 未定义的行为

1) 在前后两个序列点之间,一个标量对象所存储的值只能至多被某个表达式的求值进行一次改动,否则其行为未定义

i = ++i + i++; // 未定义的行为
i = i++ + 1; // 未定义的行为
i = ++i + 1; // 未定义的行为(在 C++11 中良构)
++ ++i; // 未定义的行为(在 C++11 中良构)
f(++i, ++i); // 未定义的行为
f(i = -1, i = -1); // 未定义的行为

2) 在前后两个序列点之间,被表达式的求值所改动的标量对象之前的值,只能为确定其所存储的值而发生访问。如果以任何其他方式访问它,则其行为未定义

cout << i << i++; // 未定义的行为
a[i] = i++; // 未定义的行为

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 1885 C++14 函数返回时自动变量的销毁顺序不明确 添加顺序规则

[编辑] 另请参阅

  • 运算符优先级,定义了表达式是如何从其源代码表示中构建而成的。
求值的顺序C 文档

[编辑] 参考资料

  • C++11 standard (ISO/IEC 14882:2011):
  • 1.9 Program execution [intro.execution]
  • 5.2.6 Increment and decrement [expr.post.incr]
  • 5.3.4 New [expr.new]
  • 5.14 Logical AND operator [expr.log.and]
  • 5.15 Logical OR operator [expr.log.or]
  • 5.16 Conditional operator [expr.cond]
  • 5.17 Assignment and compound assignment operators [expr.ass]
  • 5.18 Comma operator [expr.comma]
  • 8.5.4 List-initialization [dcl.init.list]