条款20:协助完成"返回值优化(RVO)"
《More Effective C++:35个改善编程与设计的有效方法》本书并没有第2 版,原因是当其出版之时(1996),C++ Standard已经几乎定案,本书即依当时的标准草案而写,其与现今的C++标准规范几乎相同。而且可能变化的几个弹性之处,Meyers也都有所说明与提示。本节为大家介绍条款20:协助完成"返回值优化(RVO)"。
条款20:协助完成"返回值优化(RVO)"
函数如果返回对象,对"效率狂"而言是一个严重的挫败,因为以 by-value 方式返回对象,背后隐藏的 constructor 和 destructor(见条款19)都将无法消除。问题很简单:如果为了行为正确而不得不这样,函数可返回一个对象;否则就不要那么做。如果真的决定返回对象,那就没有任何办法可以摆脱"返回一个对象"所会遭遇的命运。
考虑分数(rational numbers)的 operator* 函数:
- class Rational {
- public:
- Rational(int numerator = 0, int denominator = 1);
- ...
- int numerator() const;
- int denominator() const;
- };
- // 条款6曾解释为什么此返回值是 const。
- const Rational operator*(const Rational& lhs,
- const Rational& rhs);
甚至不必看 operator* 的函数代码,我们也知道它必须返回一个对象,因为它返回两个任意数--两个任意数哟--的乘积,operator* 如何能够在不产生新对象的情况下放置该乘积呢?不可能,所以它必须产生一个新对象并将它返回。尽管如此,C++ 程序员却像希腊神话中的赫克力斯(Herculus)一样,耗费巨大的努力企图寻找消除"by-value 返回方式"的神奇方法(见条款E23和E31)。
有时候人们会返回指针,于是导致以下这种拙劣的语法形式:
- // 一个不合理的做法,为求避免返回对象。
- const Rational* operator*(const Rational& lhs,
- const Rational& rhs);
- Rational a = 10;
- Rational b(1, 2);
- Rational c = *(a * b); // 这看起来自然吗?
此外它还引出另一个问题:调用者应该删除此函数返回的指针吗?答案通常是 yes,而那通常会导致资源泄漏(resource leaks)。(译注:因为常被忽略或遗忘。)
另一些程序员可能返回 references,于是导出一个可被接受的语法形式:
- // 一个危险(而且不正确)的做法,为求避免返回对象。
const Rational& operator*(const Rational& lhs,
const Rational& rhs);
Rational a = 10; Rational b(1, 2); Rational
c = a * b; // 看起来很合理自然。
但是这样的函数却根本无法有正确的行为。常见的做法是:
- // 一个危险(而且不正确)的做法,为求避免返回对象。
const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{ Rational result(lhs.numerator() * rhs.numerator(),
lhs.denominator() *
rhs.denominator()); return result;- }
此函数返回一个 reference,指向一个不再存活的对象。更明确地说,它返回一个 reference,指向局部对象 result,但 result 却在operator* 返回时自动被销毁了。返回一个 reference 却指向一个不再存活的对象,一点用都没有,不是吗?
请相信我:有些函数(例如,operator*)硬是得返回对象。它就是必须如此。别对它宣战,你不会赢的。
也就是说,如果函数一定得以 by-value 方式返回对象,你绝对无法消除之。这是一场错误的战争。从效率的眼光来看,你不应该在乎函数返回了一个对象,你应该在乎的是那个对象的成本几何。你需要做的,是努力找出某种方法以降低被返回对象的成本,而不是想尽办法消除对象本身(我们现在知道,那必然徒劳无功)。如果这样的对象不需要什么成本,谁在乎产生多少个呢?
我们可以用某种特殊写法来撰写函数,使它在返回对象时,能够让编译器消除临时对象的成本。我们的伎俩是:返回所谓的 constructor arguments 以取代对象。你可以这么做:
- // 返回对象:一个有效率而且正确的做法。 const Rational
operator*(const Rational& lhs,- const Rational& rhs)
- {
- return Rational(lhs.numerator() * rhs.numerator(),
- lhs.denominator() * rhs.denominator());
- }
请仔细看看被返回的表达式。看起来好像是你调用了一个 Rational constructor,事实上也的确是。通过此表达式,你产生了一个 Rational 临时对象:
- Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
而函数复制此临时对象,当做返回值。
以 constructor arguments 取代局部对象,当做返回值,这笔买卖似乎不见得多划算,因为你还是必须为"函数内的临时对象"的构造和析构付出代价,你还是必须为"函数返回对象"的构造和析构付出代价。但是你已经赚到了某些东西。C++ 允许编译器将临时对象优化,使它们不存在。于是如果你这样调用operator*:
Rational a = 10; Rational b(1, 2);
- Rational c = a * b; // 这里调用了 operator*。
你的编译器得以消除"operator* 内的临时对象"及"被 operator* 返回的临时对象"。它们可以将 return 表达式所定义的对象构造于c 的内存内。如果编译器这么做,你调用 operator* 时的临时对象总成本为 0,也就是说没有任何临时对象需要被产生出来。取而代之的是,你只需付出一个 constructor(用以产生 c)的代价。你无法做得比这更好了,因为 c 是一个命名对象,而命名对象是不能被消除的(见条款22) 。你可以将此函数声明为 inline,以消除调用 operator* 所花费的额外开销(但请先看过条款E33):
- // 函数返回一个对象:最有效率的做法。 inline const
Rational operator*(const Rational& lhs,
const Rational& rhs)- {
- return Rational(lhs.numerator() * rhs.numerator(),
- lhs.denominator() * rhs.denominator());
- }
"是啊,是啊"你低声抱怨,"优化……反优化……谁在乎编译器能够做些什么呢?我要知道的是它们真正做了些什么。真正的编译器都有做这个无聊的工作吗?"是的,此特殊的优化行为--利用函数的return 点消除一个局部临时对象(并可能用函数调用端的某对象取代)--不但广为人知而且很普遍地被实现出来。它甚至有个专属名称:return value optimization。"拥有专属名称"这一事实足以反映出它是多么被广泛运用。程序员在寻找理想的C++ 编译器时,不妨询问厂商是否支持 return value optimization。如果A厂商有,而B厂商反问"那是什么呀?",A厂商有明显的竞争优势。
回书目 上一节 下一节 |