C++17 详解 5

本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。

2. 语言阐明

学习并完全理解 C++ 是很有挑战的,许多地方都让程序员很疑惑。缺乏明确行为的一个原因,可能是赋予了编译器实现自由选择的权利。比如,允许更激进的优化,或者为了向后兼容(或者兼容 C)的需要。C++17 回顾了几个最出名的“黑洞”并把它们做了处理。

本章你将学到:

  • 什么是求值顺序,求值顺序为什么会导致非预期结果。
  • 复制消除(一种可选的优化手段,似乎在所有流行编译器上都已被实现。)
  • 异常作为函数声明的一部分。
  • (过度)对齐数据的内存分配。

2.1 更严格的表达式求值顺序

C++17 之前标准一直没有明确规定函数参数的求值顺序。呜呼哀哉

比如,这也是为什么说在 C++14 里 make_unique 不只是语法糖的原因,它还保证了内存安全:

看下边这个例子:

1
foo(make_unique<T>(), otherFunction());

和显式 new 版本:

1
foo(unique_ptr<T>(new T), otherFunction());

上边代码在 C++14 里,我们知道 new T 被保证会在 unique_ptr 构造之前执行,不过也仅此而已。new T 可能会率先执行,然后是 otherFunction(),最后才是 unique_ptr 构造。

otherFunction() 抛出异常,new T 就会导致一次内存泄漏(智能指针对象还没有创建)。但如果你用的是 make_unique,它不可能导致内存泄漏,即使执行顺序未知。

C++17 解决了这个问题,现在属性的求值顺序是“实用的”、可预测的。

译注:原文为 “C++17 addresses this issue, and now the evaluation order of attributes is “practical” and predictable.”

2.1.1 示例

在如下表达式里:

1
f(a, b, c);

a b c 的求值顺序依然不明确,但是任一参数都会在下一个参数执行前被全部计算。这点在如下的复杂表达式里至关重要:

1
f(a(x), b, c(y));

当编译器决定计算第一个参数 a(x) 时,同样需要在处理 bc(y) 之前计算 x

它同样修复了上边 make_uniqueunique_ptr<t>(new T()) 的问题——因为函数参数必须在其它参数开始执行前被完全求值。

考虑下边的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class Query
{
public:
Query& addInt(int i)
{
std::cout << "addInt: " << i << '\n';
return *this;
}
Query& addFloat(float f)
{
std::cout << "addFloat: " << f << '\n';
return *this;
}
};

float computeFloat()
{
std::cout << "computing float... \n";
return 10.1f;
}

float computeInt()
{
std::cout << "computing int... \n";
return 8;
}

int main()
{
Query q;
q.addFloat(computeFloat()).addInt(computeInt());
}

C++14 里,你可能会期望 computeInt() 后于 addFloat() 执行。不幸的是它有可能不是这样执行。下边是 GCC 4.7.3 的输出:

1
2
3
4
computing int...
computing float...
addFloat: 10.1
addInt: 8

函数调用链被明确规定为自左向右,但是内层的表达式的求值顺序是可能不一样的。更准确地来说:

表达式间的相互顺序是不确定的。

译注:原文为 “The expressions are indeterminately sequenced with respect to each other.”。

不过现在,在 C++17 里,包含嵌套表达式的函数调用链会如期运行,即自左向右:

在如下表达式里:

1
a(expA).b(expB).c(expC)

expA 会先于 b 执行。

用一个合规的 C++17 编译器编译上边的示例代码,会生成如下结果:

1
2
3
4
computing float...
addFloat: 10.1
computing int...
addInt: 8

本次修改的另一个结果是,当进行操作符重载时,其执行顺序取决于原本内置操作符的关联性。

这也是为什么 std::cout << a() << b() << c() 会按 a b c 顺序执行的原因。C++17 之前,它可以是任意执行顺序。

下边是标准描述的更多规则:

下列表达式均按先 ab 的顺序计算:

  1. a.b
  2. a->b
  3. a->*b
  4. a(b1, b2, b3) // b1, b2, b3 - 任意顺序
  5. b @= a // '@' means any operator
  6. a[b]
  7. a << b
  8. a >> b

如果你不确定自己的代码如何被计算,你最好做一次简化,把代码拆成多个清晰的语句。你可以在《Core C++ Guidelines》里找到指导,比如ES.44¹ES.44²

扩展:本修改提案:P0145R3

评论