为什么改用引用传参也等于痴人说梦
| void readFile(vector& v){ … // fill v } | 这当然可以。
但是如果遇到操作符重载呢?
| string operator+(string const& s1, string const& s2); | 而且,就算是对于readFile,原先的返回vector的版本支持
BOOST_FOREACH(int i, readFile()){ … // do sth. with i } | 改成引用传参后,原本优雅的形式被破坏了,为了进行以上操作不得不引入一个新的名字,这个名字的存在只是为了应付被破坏的形式,一旦foreach操作结束它短暂的生命也随之结束:
vector v; readFile(v); BOOST_FOREACH(int I, v){ }
// v becomes useless here | 还有什么问题吗?自己去发现吧。总之,利用引用传参是一个解决方案,但其能力有限,而且,其自身也会带来一些其它问题。终究不是一个优雅的办法。
问题是什么
《你的灯亮着吗?》里面漂亮地阐述了定义“问题是什么”的重要性。对于我们面临的临时对象的效率问题,这个问题同样重要。
简而言之,问题可以描述为:
C++没有区分copy和move语意。
什么是move语意?记得auto_ptr吗?auto_ptr在“拷贝”的时候其实并非严格意义上的拷贝。“拷贝”是要保留源对象不变,并基于它复制出一个新的对象出来。但auto_ptr的“拷贝”却会将源对象“掏空”,只留一个空壳——一次资源所有权的转移。
这就是move。
Move语意的作用——效率优化
举个具体的例子,std::string的拷贝构造函数会做两件事情:一,根据源std::string对象的大小分配一段大小适当的缓冲区。二,将源std::string中的字符串拷贝过来。
// just for illustrating the idea, not the actual implementation
string::string(const string& o) { this->buffer_ = new buffer[o.length() + 1]; copy(o.begin(), o.end(), buffer_); } | 但是假设我们知道o是一个临时对象(比如是一个函数的返回值),即o不会再被其它地方用到,o的生命期会在它所处的full expression的结尾结束的话,我们便可以将o里面的资源偷过来:
string::string(temporary string& o) { // since o is a temporary, we can safely steal its resources without causing any problem this->buffer_ = o.buffer_; o.buffer_ = 0; } | 这里的temporary是一个捏造的关键字,其作用是使该构造函数区分出临时对象(即只有当参数是一个临时的string对象时,该构造函数才被调用)。
想想看,如果存在这样一个move constructor(搬移式构造函数)的话,所有源对象为临时对象的拷贝构造行为都可以简化为搬移式(move)构造。对于上面的string例子来说,move和copy construction之间的效率差是节省了一次O(n)的分配操作,一次O(n)的拷贝操作,一次O(1)的析构操作(被拷贝的那个临时对象的析构)。这里的效率提升是显而易见且显著的。
最后,要实现这一点,只需要我们具有判断左值右值的能力(比如前面设想的那个temporary关键字),从而针对源对象为临时对象的情况进行“偷”资源的行动。
Move语意的作用——使能(enabling)
再举一个例子,std::fstream。fstream是不可拷贝的(实际上,所有的标准流对象都是不可拷贝的),因而我们只能通过引用来访问一开始建立的那个流对象。但是,这种办法有一个问题,如果我们要从一个函数中返回一个流对象出来就不行了:
// how do we make this happen? std::fstream createStream() { … } | 当然,你可以用auto_ptr来解决这个问题,但这就使代码非常笨拙且难以维护。
但如果fstream是moveable的,以上代码就是可行的了。所谓“moveable”即是指(当源对象是临时对象时)在对象拷贝语法之下进行的实际动作是像auto_ptr那样的资源所有权转移:源对象被掏空,所有资源都被转移到目标对象中——好比一次搬家(move)。move操作之后,源对象虽然还有名有姓地存在着,但实际上其“实质”(内部拥有的资源)已经消失了,或者说,源对象从语意上已经消失了。 |