nrv优化

原文地址

http://blog.csdn.net/zha_1525515/article/details/7170059


大纲:

  1.  函数返回局部对象的拷贝的一般实现方式。
  2.  NRV(Named Return Value)优化。
  3.  NRV优化触发的疑问。

一、函数返回局部对象的拷贝的一般实现方式

比如有这么一段函数定义:

[cpp] view plain copy
  1. class X;  
  2. X bar()  
  3. {  
  4.     X x1;  
  5.     // 处理 x1..  
  6.     return x1;  
  7. }  

在学习C++语法时,我们知道了。针对”Xbar()”这样的函数,是返回class X的一个对象的拷贝。其返回值是一个对象,比如叫做x2。在执行return时,x2通过调用拷贝构造函数,拷贝对象x1来实现其初始化。也就是说,这里会存在两个对象x1、x2。那么这种返回对象的拷贝,是怎么实现的呢?一般来说,C++编译器会将上段代码中bar的实现转换成如下的代码。

[cpp] view plain copy
  1. // 函数实现  
  2. void bar(X& __result)   // 加上一个额外参数  
  3. {  
  4.     // 预留x1的内存空间  
  5.     X x1;  
  6.   
  7.     // 编译器产生的默认构造函数的调用,  
  8.     x1.X::X();  
  9.   
  10.     // 处理 x1..  
  11.   
  12.     // 编译器产生的拷贝操作  
  13.     __result.X::X(x1);  
  14.   
  15.     return;  
  16. }  
  17.   
  18. // 函数调用  
  19. X x2;                   // 这里只是预留内存,并未调用初始化函数  
  20. bar(x2);  
通过上述代码,我们可见编译器对于返回对象拷贝的处理方式。

1、函数添加一个额外参数,为返回对象的引用;

2、函数调用前,先申请欲返回对象x2的内存空间;

3、将对象x2的引用传入函数中,并在函数返回前,调用x2的拷贝构造函数。

通过上述实现方式

[cpp] view plain copy
  1. X x2 = bar();  
被转换成了
[cpp] view plain copy
  1. X x2;  
  2. bar(x2);  


二、NRV(Named Return Value)优化

上面的实现中,存在着x1、x2两个对象,而x1的生命周期转瞬即逝。而且对于bar()的调用者来说,根本就没有x1这个对象,调用者想要的只有x2。这样的实现能不能够将其优化变得更快呢。编译器有一种优化方式,直接将x2替代x1。编译器转换后的伪代码如下。

[cpp] view plain copy
  1. void bar(X& __result)  
  2. {  
  3.     // 调用__result的默认构造函数  
  4.     __result.X::X();  
  5.   
  6.     // 处理__result  
  7.     return;  
  8. }  

从代码看出,这里只有一个对象,也就是传入的x2。NRV优化后的实现,比原来的实现省去了如下操作:

1)  在堆栈中预留x1的内存;

2)  调用X的默认构造函数,构造x1

3)  调用X的贝构造函数,构造x2

4)  调用x1的析构函数

5)  堆栈中回收x1的内存

但是多了一个操作,就是调用X的默认构造函数,构造x2。 对于函数的调用者(只关心x2不关心x1)来说,只有一个区别,就是x2的构造方式由调用拷贝构造函数,转变成了调用默认构造函数

三、NRV优化触发的疑问

上面的内容在《深度探索C++对象模型》中都有详细的讲解。此外书中还提到了程序员必须给class X定义拷贝构造函数才能触发NRV优化,不然还是按照最初的较慢的方式执行。可是为什么一定要定义拷贝构造函数才能触发NRV优化呢。在网上找了半天一直没有确定的答案。于是我做了如下实验。有如下代码:

[cpp] view plain copy
  1. class CTest  
  2. {  
  3. public:  
  4.     CTest()  
  5.     {  
  6.         cout << "CTest()" << this << endl;  
  7.     }  
  8.     CTest(const CTest& rcTest)  
  9.     {  
  10.         cout << "CTest(CTest)" << this << endl;  
  11.     }  
  12.     ~CTest()  
  13.     {  
  14.         cout << "~CTest()" << this << endl;  
  15.     }  
  16. private:  
  17.     int a;  
  18. };  
  19.   
  20. CTest foo()  
  21. {  
  22.     CTest oTestInFoo;  
  23.     return oTestInFoo;  
  24. }  
  25.   
  26. int main()  
  27. {  
  28.     CTest oTest = foo();  
  29.     return 0;  
  30. }  
在不同的编译环境的执行结果分别为:

VS2005(Debug)

VS2005(Release)

g++(-c -o)

也就是说,在vs2005的release环境和g++中,都触发了编译器的NRV优化。

然后,再将代码中的class CTest的拷贝构造函数去掉,执行结果依次为:

VS2005(Debug)

VS2005(Release)

g++(-c -o)

我们去掉class CTest的拷贝构造函数后,按照《深度探索》中所说,class CTest的拷贝动作只需要bitwise copy就可以实现。所以编译器也不会给其合成一个implicit的拷贝构造函数。也就是说,这个时候class CTest是没有拷贝构造的。但执行结果和去掉拷贝构造前一样,vs2005译的release程序和g++中,均使用了NRV优化。

最后将CTest的代码改为

[cpp] view plain copy
  1. class CSub  
  2. {  
  3. public:  
  4.     CSub(){}  
  5.     CSub(const CSub& rcSub)  
  6.     {  
  7.         cout << "CSub(CSub)" << endl;  
  8.     }  
  9.   
  10. };  
  11.   
  12. class CTest  
  13. {  
  14. public:  
  15.     CTest()  
  16.     {  
  17.         cout << "CTest()" << this << endl;  
  18.     }  
  19.     ~CTest()  
  20.     {  
  21.         cout << "~CTest()" << this << endl;  
  22.     }  
  23. private:  
  24.     int a;  
  25.     CSub oSub;  
  26. };  
这个时候的执行结果为:

VS2005(Debug)

VS2005(Release)

g++(-c -o)

此时因为class CTest中有oSub,且oSub需要调用其拷贝构造函数才能完成拷贝,所以编译器会给其添加一个implicit的拷贝构造函数。而就算这样,执行结果依然和前面的一样。

实验结果显示,不管是类有explicit的构造、implicit的拷贝构造还是没有拷贝构造,在vs2005的Release和g++下都会触发NRV优化,在vs2005(Debug)下都没有NRV优化。所以可以得出结论,在这两个编译器中,NRV优化和拷贝构造函数是否定义没关系

那么为什么《深度探索》的作者会说有关系呢。网上找到了一种说法,我觉得比较有道理(传送门)截取其中的话就是“早期的 cfront需要一个开关来决定是否应该对代码实行NRV优化,这就是是否有客户(程序员)显式提供的拷贝构造函数:如 果客户没有显示提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但 如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。

我认为,因为作者在书中一直都是以cfront来举例说明的,所以其才会有NRV开关的说法。

其实不管cfront如何,现在已经确定的是vs2005(Release)和g++都会执行NRV优化。而NRV优化会导致原本预想中的调用拷贝构造函数变成调用别的构造函数(视函数中的对象调用的构造函数而定)。这一点一定要注意,因为一旦这个时候,拷贝构造函数和别的构造函数提供的功能不同(其实一直都不应该这样),会导致debug和release出现执行结果不同的情况。

你可能感兴趣的:(nrv优化)