前言
考虑如下程序:
1 |
|
我们可能会做出如下假设:
- foo()一旦被调用,就会传回xx的值。
- 如果class X定义了一个copy constructor,当foo()被调用时,该copy constructor一定会被调用。
第一个假设的正确性视class X的定义而定。第二个假设的正确性,则视编译器的优化程度而定。我们甚至可以假设,在一个高品质C++编译器中,上述两点假设均不正确。
Explicit Initialization
已知存在如下定义:
1 | X x0; |
下面有三个定义,每一个都显式地以x0的值来初始化class object:
1 | void foo_var(){ |
必要的程序转化有两个阶段:
- 重写每一个定义,剥离其中的初始化操作
- class的copy constructor调用操作被插入程序
经过转化后的foo_bar()程序看起来如下所示:
1 | void foo_bar(){ |
Argument Initialization
C++ standard中提及,将一个class object作为函数参数传递(或作为函数返回值)等价于以下形式的初始化:
1 | X xx = arg; |
因此,若有函数及调用语句如下:
1 | void foo(X x0); |
将会要求local instance x0以memberwise的方式将xx作为初值。
在编译器实现技术上,有一种策略是导入临时对象,并调用copy constructor将它初始化,然后将临时对象转交函数:
1 | X _temp0; |
然而这样的转换并不完备,问题出在foo的声明式中。临时对象已经得到了正确的初值,但传入函数作为参数时又再一次导致了临时对象的生成。为了保证函数被正确调用,编译器会修改foo的声明,其形参会由一个class object变为相应的reference:
1 | void foo(X& x0); |
当foo完成后,临时对象将调用X::~X(),确保自身被析构。
另有一种编译器实现策略:copy construct,它将实际参数直接建构在其应该的位置上,在函数返回之前,局部对象(不同于刚才的临时对象)会被析构(我认为此法就是真正的建立一个object,然后在使用完毕后析构之)。
Return Value Initialization
考虑如下函数定义:
1 | X bar(){ |
我们需要关注的问题是bar()的返回值将如何从局部对象xx中拷贝。一种解决方案是双阶段转化:
- 首先加上一个额外参数,其类型是class object的一个reference,该参数用来放置被copy construct得到的返回值。
- 在return指令前插入一个copy constructor操作,将局部对象的copy作为引用初值传递回去。
转换后的bar函数大致如下:
1 | void bar(X& _result){ |
同时现在的bar调用操作也被转换,以确保和新的定义匹配:
1 | //开发者撰写 |
对于返回值的相关使用也被改写:
1 | //开发者撰写 |
同样的,如果程序声明了一个函数指针:
1 | //开发者撰写 |
Optimization at the User Level
这种做法就是More Effective C++提及的RVO:
1 | X bar(const T &y,const T &z){ |
如果编译器对RVO作出改写,那么应该有形式如下:
1 | X _result; |
仅仅付出了一个构造函数的代价,而这正是我们想要的。
Optimization at the Compiler Level
在一个诸如bar()的函数中,所有的return指令返回相同的named value,因此编译器可能会自己作出优化:方法是以result参数取代named return value:
1 | X bar(){ |
(此种策略就是把xx转换为了_result)
这种编译器优化操作被称为Named Return Value(NRV)。
NRV受到的批判
尽管NRV优化提供了重大的效率改善,但它仍然饱受批判,其原因大致有三:
- 优化由编译器自主执行,其执行程度不可控(很少有编译器会说明其实现程度或是否实现)。
- 如果函数相当复杂,优化就变得难以执行,许多人认为这种情况应当使用RVO取代之。
- 优化可能破坏了原有的程序
一般而言,当一个class object作为另一个class object的初值这种情况发生时,C++允许编译器具有较大的弹性发挥空间。其优点在于机器码产生时效率得到了明显的提高,但缺点就是你无法清楚你的代码究竟被编译成为了何种形式。
是否需要定义copy constructor
1 | class Point3d{ |
上述class有必要定义copy constructor吗?如果不定义,那么编译器会将default copy constructor视作trivial,其memberwise初始化操作会导致bitwise copy,这无疑兼具高效性与安全性。
那么上述class就一定没有必要定义copy constructor吗?答案是否定的。如果class object多次以传值的姿态作为函数参数,又或者作为函数返回值,那么explicit copy constructor有助于编译器执行NRV优化,当然我们也可以显式地使用RVO优化。
总结
copy constructor的使用多多少少使得编译器对你的代码做出了转化,如果我们能够了解那些转换,以及copy constructor优化后的可能状态,就可以对程序的执行效率有清楚的认知与控制。