More Effective C++ 条款20 协助完成"返回值优化(RVO)"
1. 函数如果返回对象,就会产生临时对象(见条款19)的构造,析构等过程,考虑以下重载的operator*:
class Rational{ public: int getNumerator(){return numerator;} int getDenomerator(){return denominator;} Rational(int numerator,int denomimator); ... private: int numerator; int denominator; } Rational operator*(const Rational& lhs,const Rational& rhs){ Rational result(lhs.getNumerator()*rhs.getNumerator()+lhs.getDenominator()+rhs.getDenominator()); return result;//result是Rational类型的对象,用于存储结果 }
由于operator*要返回一个Rational对象,那么就涉及到临时对象的构造和析构问题:调用operator*时,编译器需要构造一个临时的Rational对象用于存储result的内容(因为程序一operator*的作用域result就被销毁),相应的也需要适时销毁该临时对象,这就导致了额外的成本.
令函数返回指针来消除临时对象的方法是行不通的.实际上,令函数返回指针或引用是一个不好的习惯:如果返回的指针指向的是函数内创建的non-static对象,那么当离开函数作用域时,该对象就被销毁,任何企图通过指针访问其指向的内存的行为都会导致程序错误;如果函数返回的是heap对象,那么就增加了额外的手动释放内存的负担.因此应尽量避免令函数返回指针.返回引用的缺点类似.
2. 大多数编译器具有一种优化方法——RVO(return value optimization,返回值优化),如果函数返回匿名对象,那么函数就有可能避免临时对象的构造,也就是说,如果operator*的实现像这样:
Rational operator*(const Rational&lhs,const Rational&rhs){ return Rational(lhs.getNumerator()*rhs.getNumerator() +lhs.getDenominator()+rhs.getDenominator()); }
函数返回一个临时对象就传达给编译器这样一个信息:允许编译器在合适的时候采取RVO优化将消除临时对象的构造和析构成本,于是像这样的调用:
Rational c=a*b;//a和b是Rational型对象
编译器可以直接在c上构造a和b相乘的结果而不需要先将结果存储在一临时对象中,然后再拷贝给a(方法可能是将a作为参数传入operator*).这就是RVO:返回值优化.
3. 要实现RVO,编译器通常要求函数返回匿名对象,新式的编译器支持其进化版——NRV(named return value),它可以对具名返回值做优化,消除构造和析构临时对象的成本,也就是说,即使对第一种operator*实现版本,支持NRV的编译器仍然可以对于以下语句实行优化,消除临时对象的构造和析构:
Rational c=a*b;//a和b是Rational型对象
据说vc 8.0以上的版本支持NRV优化(http://www.cnitblog.com/luckydmz/archive/2011/10/25/76193.html)
其实RVO要求函数返回匿名对象是有理由的:如果函数返回匿名对象,那么就说明程序员只是利用该匿名对象存储返回值,而并不打算用该匿名对象做其他事情,因此编译器可以将其视为一个允许它做RVO优化的信号,而NRV优化虽然可以对具名返回值做优化,以消除匿名对象的成本,这提高了效率,但有可能是不是程序员的原意.
对于NRV优化的深入了解见《深入探索C++面向对象模型》第二章
4. C++11引入了右值引用与转移语义,其目的不是消除临时对象的构造和析构,而是通过右值引用绑定到右值来延长临时对象生存期,从而直接使用临时对象,这种方法与RVO异曲同工:前者是直接使用临时对象,不再构造新对象;后者避免构造临时对象,将本应构造在临时对象上的内容直接构造到新对象上.