内存序学习笔记(一)——表达式求值顺序

你知道为什么 i = i++ + 2 在 C++17 前行为未定义吗?

你知道为什么 func(a(), b()) 中,a()b() 的执行顺序为什么不是确定的吗?

这篇文章可以解答你的疑惑。

注:内容中包含作者自行理解的表述,请谨慎参考。

表达式求值

每个表达式的求值包括:

  • 值计算:计算表达式所返回的值。
  • 引发副作用:访问(读或写)volatile 泛左值所指代的对象,修改(写入)对象,调用库 I/O 函数,或调用任何做出这些操作的函数。
表达式副作用
访问 volatile 对象可能将寄存器中的该变量值更新为内存中的值
修改对象除了对象被修改,程序的状态也有略微改变
I/O对外部环境产生影响
包括上述所有表达式的函数如上

例如,表达式 a++值计算指这个表达式的返回值是 a,引发副作用a 的值加了 1。但在这个例子中,副作用是已定义的,是用户期望的。

顺序

按顺序早于(sequenced before)

  • A 按顺序早于 B(等价地,B 按顺序晚于 A),则 A 的求值会在 B 的求值开始前完成。
  • A 不按顺序早于 B 而 B 不按顺序早于 A:
    • 无顺序(unsequenced),则它们以任何顺序进行,并且同线程内,编译器可以将两者 CPU 指令交错。
    • 顺序不确定(indeterminately sequenced),它们以任何顺序进行,但不可重叠。下次求值顺序可以相反。

注意无顺序顺序不确定的区别。由此推出,顺序不确定早于都属于已定义的顺序。只有无顺序是未定义的。

求值顺序规则

将在另一篇文章中说明以下相关知识点:完整表达式、弃值表达式、成分表达式(子表达式)、指名函数表达式、成员指针表达式、复合赋值表达式。

编号表达式类型操作(早于)对象
1完整表达式值计算、副作用下一个完整表达式
2运算符的操作数值计算运算符结果
3函数实参值计算、副作用函数体内任何语句
4(内建)后自增、后自减值计算副作用
5(内建)前自增、前自减副作用值计算
6&& || ,(逗号)的左操作数值计算、副作用右操作数
7与条件运算符(?:)第一表达式值计算、副作用第二、三表达式
8(内建)赋值运算符、复合赋值运算符的左右操作数值计算副作用(修改左操作数)
8.1(内建)赋值运算符、复合赋值运算符副作用(修改左操作数)值计算(返回引用之前)
9列表初始化的子句值计算、副作用其后的子句
10函数调用表达式中,指名函数表达式值计算、副作用 C++17起实参、默认形参
11下标表达式 E1[E2] 中,E1值计算、副作用 C++17起E2
12成员指针表达式 E1.*E2E1->*E2 中,E1值计算、副作用 C++17起E2
13移位运算符表达式 E1 << E2E1 >> E2 中,E1值计算、副作用 C++17起E2
14简单赋值表达式 E1 = E2 或复合赋值表达式 E1 @= E2 中,E2值计算、副作用 C++17起E1
  1. 如果一个函数调用和一个表达式(可以是另一个函数调用)没有明确顺序,二者的求值顺序不确定。(程序必须表现为如同一次函数调用的 CPU 指令,不会与其它表达式求值指令交错。C++17起std::execution::par_unseq 例外)
  2. new() 的调用相对于 new 表达式中各实参的求值,是顺序不确定的(C++17前);顺序早于它(C++17起)。
  3. 函数返回值的复制按顺序早于 return 语句 末尾处对所有临时量的销毁。后者又早于该块中所有局部变量的销毁。
    int add(int a, int b) {
        return a + b;
    }
    
    int main() {
        int c = add(1, 2);
    }
    
    `add()` 的结果复制到 `c` 早于 `a+b` 结果临时量的销毁
    后者又早于局部变量 `a,b` 的销毁
    
    
  4. C++17起 函数调用中,每个形参的值计算和副作用(二者作为一个整体)与其它任何形参相比是顺序不确定的。
  5. C++17起 重载的运算符遵循它所重载的内建运算符的定序规则。
  6. C++17起 列表初始化逗号分隔的每个表达式,如同函数调用一般求值(顺序不确定)。

未定义行为

注:切记未定义顺序顺序不确定的区别。

  • 如果某个内存位置上的一项副作用相对于同一个内存位置上的另一副作用是无顺序的,那么它的行为未定义。

例 1:

i = ++i + 2;  // OK

++i 副作用早于求值(第 5 条),等号右边的值计算早于左边副作用(赋值)(第 8 条)。执行顺序为:

++i 副作用 > ++i 求值 > ++i + 2 求值 > i = ++i + 2 副作用 > i = ++i + 2 求值。


例 2:

i = i++ + 2; // C++17 前行为未定义

C++17前,i++ 求值早于副作用(第 6 条),两边的值计算早于 i = i++ + 2 的副作用(第 8 条),而没有定义两个副作用的顺序,故而 i 值不确定,行为未定义。
C++17起,i++ 的副作用早于 i = i++ +2 的副作用(第 14 条)。因此可能(“可能”是因为i++2 的求值顺序是不确定的)的执行顺序为:

i++ 求值 > i++ + 2 求值 > i++ 副作用 > i = i++ + 2 副作用 > i = i++ + 2 求值


例 3:

f(i = -2, i = -2); // C++17 前行为未定义

C++17新增了一项规则(第 18 条),它规定了形参求值和副作用是顺序不确定的,此前未定义。此前两个 i = -2 不仅顺序不一定,甚至 CPU 指令可能交错。此后尽管顺序不确定,但 CPU 指令不会交错。因此执行顺序可能是:

i = -2 副作用 > i = -2 求值 > i = -2 副作用 > i = -2 求值 > f(i = -2, i = -2) 副作用 > f(i = -2, i = -2) 求值


例 4:

f(++i, ++i);       // C++17 前行为未定义,C++17 起未指明

同上,C++17前,++i++i 的副作用顺序是未定义的。C++17后(第 18 条),不管编译器优先执行哪个 ++i 都是符合规则的。但由于两个 ++i 顺序不确定,所以该表达式的值计算和副作用未明确。


例 5:

i = ++i + i++;     // 行为未定义

同例 2,i++i = ++i + i ++ 的副作用顺序未定义。

序列点规则(C++11前)

C++11 前没有 C++11起一般完备的规则,表达式求值的顺序规定依靠序列点定义。

C++11前的定义

序列点 (sequence point)是执行序列中的点,在该点所有来自序列中先前求值的副作用都已经完成,而后继求值的副作用都尚未开始。

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++;     // 未定义行为
i = i++ + 1;       // 未定义行为
i = ++i + 1;       // 未定义行为
++ ++i;            // 未定义行为
f(++i, ++i);       // 未定义行为
f(i = -1, i = -1); // 未定义行为
  1. 前后序列点间,访问表达式求值所修改的在同一个内存位置中的任何对象的先前值,必须只为确定要存储的值。如果以其他任何方式访问,那么行为未定义。
cout << i << i++; // 未定义行为
a[i] = i++;       // 未定义行为
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值