首页 > 代码库 > 临时对象与NRV技术

临时对象与NRV技术

  《More Effective C++》中讲到,在C++中真正的临时对象是看不见的,它们不出现在你的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象,这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时。

1 size_t countChar(const string& str, char ch);2 3 int main()4 {    5     char c;      6     cin >> c >> setw(MAX_STRING_LEN) >> buffer;      7     cout << "There are " << countChar(buffer, c)<< " occurrences of the character " << c<< " in " << buffer << endl;8 }
View Code

程序中countChar 第一个入参类型是 const string&,而调用是传入的是buffer[],一个字符数组,此时编译器会通过以buffer做为参数调用string的构造函数来初始化这个临时对象。countChar的参数str被绑定在这个临时的string对象上。当countChar返回时,临时对象自动释放。这样的类型转换很方便,但是从效率的观点来看,临时string对象的构造和释放是不必要的开销。仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生。

1 void uppercasify(string& str);2 3 int main()4 {5     char subtleBookPlug[] = "Effective C++";      6     uppercasify(subtleBookPlug); // 错误!7 }
View Code

程序line7报错,原因是 uppercasify 函数可能对入参进行修改,而实参是 char 数组,所以在调用函数时会创建一个临时对象,而临时对象具有 const 属性,函数的入参是non-const的引用,无法进行赋值,所以C++语言禁止为非常量引用(reference-to-non-const)产生临时对象。

  建立临时对象的第二种环境是函数返回对象时:const Number operator+(const Number& lhs, const Number& rhs); 一个返回对象的函数很难有较高的效率,因为传值返回会导致调用对象内的构造和析构函数,这种调用是不能避免的。以某种方法返回对象,能让编译器消除临时对象的开销,这样编写函数通常是很普遍的。这种技巧是返回constructor argument而不是直接返回对象。

1 const Rational operator*(const Rational& lhs, const Rational& rhs) 2 { 3     return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); 4 }
View Code

返回constructor argument而不出现局部对象,C++规则允许编译器优化不出现的临时对象(temporary objects out of existence)。

1 int main()2 {3     Rational a = 10; 4     Rational b(1, 2); 5     Rational c = a * b;6     7     return 0;8 }
View Code

程序line6,编译器就会被允许消除在operator*内的临时变量和operator*返回的临时变量。它们能在为目标c分配的内存里构造return表达式定义的对象。如果你的编译器这样去做,调用operator*的临时对象的开销就是零:没有建立临时对象。你的代价就是调用一个构造函数――建立c时调用的构造函数。而且你不能比这做得更好了,因为c是命名对象,命名对象不能被消除。

  函数返回对象时,临时对象的创建和销毁是不必要的开销,那么可以使用NRV技术来减少这些不必要的开销,提高程序的效率。Named Return Value Optimization具名返回值优化,是编译器的一项优化操作,被视为标准C++便一起的一个义不容辞的优化操作,引用《深度探索C++对象模型》中的例子:

 1 X bar() 2 { 3     X xx; 4     //...处理xx 5     return xx; 6 } 7 //编译器把其中的xx以__result取代: 8 void bar(X &__result) 9 {10     //default constructor 调用11     //c++伪代码12     __result.X::X();13     14     //...直接处理 __result15     16     return;17 }
View Code

这里编译器以result参数取代了named return value,返回时不会调用copy constructor拷贝。更具体的描述如下

A a = f();

此处f()函数创建了一个A类对象b,然后返回对象b,在对象b返回时,一般要调用拷贝构造函数,把函数f()里边的局部对象b拷贝到函数外部的对象a,但如果用了NRV优化,那么就不必调用拷贝构造函数,编译器可以这么做:把a的地址传递给f(),不让f()创建返回的对象b,用a代替b的地址,这样当要返回对象b的时候,就不必拷贝了,因为b就是a。

  《深度探索C++对象模型》中提到要激活NRV,必须提供copy construct函数,下面是VS2010中的测试:

 1 using namespace std; 2 class Complex 3 { 4     friend Complex operator+(const Complex&, const Complex&); 5 public: 6     Complex(double r=0.0, double i=0.0):real(r),imag(i) 7     { 8         cout<<"Complex real="<<r<<", imag="<<i<<endl; 9     }10     Complex(const Complex& c):real(c.real), imag(c.imag)11     {12         cout<<"Complex(const Complex& c), real="<<real<<", imag="<<imag<<endl;13     }14     Complex& operator=(const Complex& c)15     {16         cout<<"Complex& operator=, real="<<c.real<<", imag="<<c.imag<<endl;17         if(this == &c)18             return *this;19         real = c.real;20         imag = c.imag;21     }22     ~Complex()23     {24         cout<<"~Complex(), real="<<real<<", imag="<<imag<<endl;25     }26 private:27     double real;28     double imag;29 };30 Complex operator+(const Complex& a, const Complex& b)31 {32 //  Complex retVal(a.real + b.real, a.imag + b.imag);33 //  return retVal;34     return Complex (a.real + b.real, a.imag + b.imag);35 }36  37 int _tmain(int argc, _TCHAR* argv[])38 {39     Complex a(12, 23), b(23, 34);40     Complex c = a+b;41  42     cout<<"################################"<<endl;43  44     return 0;45 }
View Code

上面的代码在Debug/Release选项下结果相同:

?

而如果将 Complex operator+ 函数修改:

1 Complex operator+(const Complex& a, const Complex& b)2 {3     Complex retVal(a.real + b.real, a.imag + b.imag);4     return retVal;5 }
View Code

Debug/Release选项下输出的结果不同:

Debug:

Release:


 

临时对象与NRV技术