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)
时,同样需要在处理 b
或 c(y)
之前计算 x
。
它同样修复了上边 make_unique
和 unique_ptr<t>(new T())
的问题——因为函数参数必须在其它参数开始执行前被完全求值。
考虑下边的情况:
1 |
|
C++14 里,你可能会期望 computeInt()
后于 addFloat()
执行。不幸的是它有可能不是这样执行。下边是 GCC 4.7.3 的输出:
1 | computing int... |
函数调用链被明确规定为自左向右,但是内层的表达式的求值顺序是可能不一样的。更准确地来说:
表达式间的相互顺序是不确定的。
译注:原文为 “The expressions are indeterminately sequenced with respect to each other.”。
不过现在,在 C++17 里,包含嵌套表达式的函数调用链会如期运行,即自左向右:
在如下表达式里:
1 | a(expA).b(expB).c(expC) |
expA
会先于 b
执行。
用一个合规的 C++17 编译器编译上边的示例代码,会生成如下结果:
1 | computing float... |
本次修改的另一个结果是,当进行操作符重载时,其执行顺序取决于原本内置操作符的关联性。
这也是为什么 std::cout << a() << b() << c()
会按 a
b
c
顺序执行的原因。C++17 之前,它可以是任意执行顺序。
下边是标准描述的更多规则:
下列表达式均按先
a
后b
的顺序计算:
a.b
a->b
a->*b
a(b1, b2, b3) // b1, b2, b3 - 任意顺序
b @= a // '@' means any operator
a[b]
a << b
a >> b
如果你不确定自己的代码如何被计算,你最好做一次简化,把代码拆成多个清晰的语句。你可以在《Core C++ Guidelines》里找到指导,比如ES.44¹ 和 ES.44²。
扩展:本修改提案:P0145R3。