More Effective C++----(20)协助完成返回值优化

Item M20:协助完成返回值优化

一个返回对象的函数很难有较高的效率,因为传值返回会导致调用对象内的构造和析构函数(参见条款M19),这种调用是不能避免的。问题很简单:一个函数要么为了保证正确的行为而返回对象要么就不这么做。如果它返回了对象,就没有办法摆脱被返回的对象。就说到这。

考虑rational(有理数)类的成员函数operator*:(返回类型为const是为了防止连续对操作符的操作,一是返回的对象是一个临时对象,多个操作是在其临时对象上的操作,二是不符合内置类型的要求)

class Rational {
public:
  Rational(int numerator = 0, int denominator = 1);
  ...
  int numerator() const;
  int denominator() const;
};

// 有关为什么返回值是const的解释,参见条款M6,
const Rational operator*(const Rational& lhs,
                         const Rational& rhs);

条款M6<http://blog.csdn.net/qianqin_2014/article/details/51318594>

甚至不用看operator*的代码,我们就知道它肯定要返回一个对象,因为它返回的是两个任意数字的计算结果。这些结果是任意的数字。operator*如何能避免建立新对象来容纳它们的计算结果呢?这是不可能的,所以它必须得建立新对象并返回它。不过C++程序员仍然花费大量的精力寻找传说中的方法,能够去除传值返回的对象(参见Effective C++ 条款23和条款31)。

有时人们会返回指针,从而导致这种滑稽的句法:

// 一种不合理的避免返回对象的方法
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 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;//WQ加注 返回时,其指向的对象已经不存在了
}

这个函数返回的引用,其指向的对象已经不存在了。它返回的是一个指向局部对象result的引用,当operator* 退出时result被自动释放。返回指向已被释放的对象的引用,这样的引用绝对不能使用。(返回*this,参考C++
Primer

相信我:一些函数(operator*也在其中)必须要返回对象。这就是它们的运行方法。不要与其对抗,你不会赢的。

你消除传值返回的对象的努力不会获得胜利。这是一场错误的战争。从效率的观点来看,你不应该关心函数返回的对象,你仅仅应该关心对象的开销。你所应该关心的是把你的努力引导到寻找减少返回对象的开销上来,而不是去消除对象本身(我们现在认识到这种寻求是无用的)。如果没有与这些对象相关的开销,谁还会关心有多少对象被建立呢?

以某种方法返回对象,能让编译器消除临时对象的开销,这样编写函数通常是很普遍的。这种技巧是返回constructor argument而不是直接返回对象,你可以这样做:

// 一种高效和正确的方法,用来实现
// 返回对象的函数
const Rational operator*(const Rational& lhs,
                         const Rational& rhs)
{
  return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}

仔细观察被返回的表达式。它看上去好象正在调用Rational的构造函数,实际上确是这样。你通过这个表达式建立一个临时的Rational对象,

Rational(lhs.numerator() * rhs.numerator(),
         lhs.denominator() * rhs.denominator());

并且这是一个临时对象,函数把它拷贝给函数的返回值。

返回constructor argument而不出现局部对象,这种方法还会给你带来很多开销,因为你仍旧必须为在函数内临时对象的构造和释放而付出代价,你仍旧必须为函数返回对象的构造和释放而付出代价。但是你已经获得了好处。C++规则允许编译器优化不出现的临时对象(temporary
objects out of existence)。
因此如果你在如下的环境里调用operator*:

Rational a = 10;
Rational b(1, 2);
Rational c = a * b;                          // 在这里调用operator*

编译器就会被允许消除在operator*内的临时变量和operator*返回的临时变量。它们能在为目标c分配的内存里构造return表达式定义的对象。如果你的编译器这样去做,调用operator*的临时对象的开销就是零:没有建立临时对象。你的代价就是调用一个构造函数――建立c时调用的构造函数。而且你不能比这做得更好了,因为c是命名对象,命名对象不能被消除(参见条款M22)。不过你还可以通过把函数声明为inline来消除operator*的调用开销(不过首先参见Effective
C++ 条款33):

// the most efficient way to write a function returning
// an object
inline const Rational operator*(const Rational& lhs,
                                const Rational& rhs)
{
  return Rational(lhs.numerator() * rhs.numerator(),
                  lhs.denominator() * rhs.denominator());
}

“好,不错”,你嘀咕地说,“优化,谁关心编译器能做什么?我想知道它们确实做了什么,Does any of this nonsense work with real compilers?” It does。这种特殊的优化――通过使用函数的return 位置(或者在函数被调用位置用一个对象来替代)来消除局部临时对象――是众所周知的和被普遍实现的。它甚至还有一个名字:返回值优化(return
value optimization)
(WQ加注:在《深度探索C++物件模型》中有更多更详细的讲述,它叫之为named return value optimization。但注意,这种优化对普通的赋值运算无效,编译器不能够用拷贝构造函数取代赋值运算动作,最终结论是:在确保语意正确的前题下没有更好的优化可能了)。实际上这种优化有自己的名字本身就可以解释为什么它被广泛地使用。寻找C++编译器的程序员会问销售商编译器是否有返回值优化功能。如果一个销售商说有而另一个问“那是什么东西?”,第一个销售商就会有明显的竞争优势。啊,资本主义,有时你实在应该去爱它。(谨代表作者观点,译者坚决拥护四项基本原则
译者注 :-) )

附:

文中最后一段黑体部分如何翻译,我有些拿不准,请高手告知,为了容易理解,我在此附上此文最后一段的英文原文:

"Yeah, yeah," you mutter, "optimization, schmoptimization. Who cares what compilers can do? I want to know what they do do. Does any of this nonsense work with real compilers?" It does. This particular optimization — eliminating a local temporary by using a
function‘s return location (and possibly replacing that with an object at the function‘s call site) — is both well-known and commonly implemented. It even has a name: the return value optimization. In fact, the existence of a name for this optimization may
explain why it‘s so

widely available. Programmers looking for a C++ compiler can ask vendors whether the return value optimization is implemented. If one vendor says yes and another says "The what?," the first vendor has a notable competitive advantage. Ah, capitalism. Sometimes
you just gotta love it.

总结:返回值优化!

时间: 2024-10-14 01:00:45

More Effective C++----(20)协助完成返回值优化的相关文章

C++返回值优化RVO

返回值优化,是一种属于编译器的技术,它通过转换源代码和对象的创建来加快源代码的执行速度.RVO = return value optimization. 测试平台:STM32F103VG + Keil 5.15 背景:我们有个MacAddress::ToArray byte* MacAddress::ToArray() const { return (byte*)&Value; } 因为封装需要,打算返回字节数组类ByteArray的对象,于是有 ByteArray MacAddress::To

C++返回值优化

返回值优化(Return Value Optimization,简称RVO)是一种编译器优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用于返回,那么这个临时对象会消耗一个构造函数(Constructor)的调用.一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价. 经过返回值优化,就可以将成本降低到一个构造函数的代价.这样就省去了一次拷贝构造函数的调用和依次析构函数的调用. 例子如下: class MyString {

转:C++中临时对象及返回值优化

http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象. 通常出现在以下两种情况: (1)为了使函数调用成功而进行隐式类型转换的时候. 传递某对象给一个函数,而其类型与函数的形参类型不同时,如果可以通过隐式转化的话可以使函数调用成功,那么此时会通过构造函数生成一个临时对象,当函数返回时临时对象即自动销毁.如下例: //计算字符ch

命名的返回值优化(Named Return Value optimization (NRVO))

命名的返回值优化: 针对返回一个局部的变量的优化,可以直接用返回的结果对象直接替代局部变量,从而减少了一个复制拷贝,从而提高效率. 比如 一个函数如下: X bar() { X xx; // .. 处理xx return xx; } 而在编译器看来则是如下的代码: // 此处的_result是一个在调用该函数时产生的一个临时对象 // , 然后将该对象传入,用以接受结果 void bar(X & __result) { X xx; // 调用xx的构造函数 xx.X::X(); // .. 处理

[转] C++中临时对象及返回值优化

http://www.cnblogs.com/xkfz007/articles/2506022.html 什么是临时对象? C++真正的临时对象是不可见的匿名对象,不会出现在你的源码中,但是程序在运行时确实生成了这样的对象. 通常出现在以下两种情况: (1)为了使函数调用成功而进行隐式类型转换的时候. 传递某对象给一个函数,而其类型与函数的形参类型不同时,如果可以通过隐式转化的话可以使函数调用成功,那么此时会通过构造函数生成一个临时对象,当函数返回时临时对象即自动销毁.如下例: //计算字符ch

060、Java中定义有返回值有参数的方法

01.代码如下: package TIANPAN; /** * 此处为文档注释 * * @author 田攀 微信382477247 */ public class TestDemo { public static void main(String[] args) { int result = add(10, 20); // 方法的返回值可以进行接收 System.out.println("计算结果:" + result); System.out.println("计算结果:

C++返回值为对象时复制构造函数不执行怎么破

先说点背景知识,调用复制构造函数的三种情况: 1.当用类一个对象去初始化另一个对象时. 2.如果函数形参是类对象. 3.如果函数返回值是类对象,函数执行完成返回调用时. 在辅导学生上机时,有同学第3点提出异议.有教材上的例题为证: #include <iostream> using namespace std; class Point //Point 类的定义 { public: Point(int xx=0, int yy=0) { x = xx; //构造函数,内联 y = yy; } P

参数返回值及NRV优化(named return value optimization)

C++11中的移动构造函数又把NRV优化翻出来了,都是采用临时中间值优化,两者不能共存. 参数传递如何实现? [实现模型1]引入临时对象,使用拷贝构造函数初始化.然后利用bitwise copy将其拷贝到x0的位置.比如: void foo( X x0 ); X xx; foo( xx ); 改写成 X __temp0; __temp0.X::X ( xx ); foo( __temp0 );还有一件事需要做,修改foo的声明,可以避免bit-wise copy的那一步. void foo( X

Effective Modern C++:05右值引用、移动语义和完美转发

移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引用是将这两个不相关的语言特性连接起来的底层语言机制,正是它使得移动语义和完美转发成了可能. 23:理解std::move和std::forward std::move并不进行任何移动,std::forward也不进行任何转发.这两者在运行期都无所作为,它们不会生成任何可执行代码.实际上,std::m