AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

《C++0x漫谈》系列之:右值引用

51自学网 2015-09-06 http://www.51zixue.net

 

  最初的例子——完美解决方案

  在先前的那个例子中

vector<int> v = readFile();

  有了move语意的话,readFile就可以简单的改成:

std::vector<int> readFile()
{
std::vector<int> retv;
… // fill retv
return std::move(retv); // move retv out
}

  std::move以后再介绍。目前你只要知道,std::move就可以把retv掏空,即搬移出去,而搬家的最终目的地是v。这样的话,从内存分配的角度讲,只有retv中进行的内存分配,在从retv到返回的临时对象,再从后者到目的地v的“move”过程中,没有任何的内存分配(我是指vector内的缓冲区分配),取而代之的是,先是retv内的缓冲区被“转移”到返回值临时对象中,然后再从临时对象中转移到v中。相比于以前的两次拷贝而言,两次move操作节省了多少工作量呢?节省了两次new操作两次delete操作,还有两次O(n)的拷贝操作,这些操作整体的代价正比于retv这个vector的大小。难怪人们说临时对象效率问题是C++的肿瘤(wart)之一,难怪C++标准都要不惜代价允许(N)RVO。

  如何支持move语意

  根据前面的介绍,你想必已经知道。实现move语意的最关键环节在于能够在编译期区分左值右值(也就是说识别出临时对象)。

  现在,回忆一下,在文章的开头我曾经提到:

  我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…

  为什么这么说?

  现行标准(C++03)下的方案

  要想区分左值右值,只有通过重载:

void foo(X const&);
void foo(X&);

  这样的重载显然是行不通的。因为X const&会把non-const临时对象一并吞掉。

  这种做法的问题在于。X&是一个non-const引用,它只能接受non-const左值。然而,C++里面的值一共有四种组合:

  const non-const
  lvalue
  rvalue

  常量性(const-ness)与左值性(lvalue-ness)是正交的。

  non-const引用只能绑定到其中的一个组合,即non-const lvalue。还剩下const左值,const右值,以及我们最关心的——non-const右值。而只有最后一种——non-const右值——才是可以move的。

  剩下的问题便是如何设计重载函数来搞定const左值和const右值。使得最后只留下non-const右值。

  所幸的是,我们可以借助强大的模板参数推导机制:

// catch non-const lvalues
void foo(X&);
// catch const lvalues and const rvalues
template<typename T>
void foo(T&, enable_if_same<T, const X>::type* = 0);
void foo( /* what goes here? */);

  注意,第二个重载负责接受const左值和const右值。经过第一第二个foo重载之后剩下来的便是non-const rvalue了。

  问题是,我们怎么捕获这些non-const rvalue呢?根据C++03,const-const rvalue只能绑定到const引用。但如果我们用const引用的话,就会越俎代庖把const左右值一并接受了(因为在模板函数(第二个重载)和非模板函数(第三个重载)之间编译器总是会偏好非模板)。

  那除了用const引用,难道还有什么办法来接受一个non-const rvalue吗?

  有。

  假设你的类型为X,那么只要在X里面加入一点料:

struct ref_x
{
ref_x(X* p) : p_(p) {}
X* p_;
};
struct X
{
// original stuff

// added stuff, for move semantic
operator ref_x()
{
return ref_x(this);
}
};

  这样,我们的第三个重载函数便可以写成:

void foo(ref_x rx); // accept non-const temporaries only!

  Bang! 我们成功地在C++03下识别出了moveable的non-const临时对象。不过前提是必须得在moveable的类型里加入一些东西。这也正是该方案的最大弊病——它是侵入式的(姑且不说它利用了语言的阴暗角落,并且带来了很大的编码复杂度)。

  C++09的方案

  实际上,刚才讲的这个利用重载的方案做成库便是Andrei的mojo框架。mojo框架固然精巧,但复杂性太大,使用成本太高,不够优雅直观。所以语言级别的支持看来是必然选择(后面你还会看到,为了支持move语意而引入的新的语言特性同时还支持了另一个广泛的问题——完美转发)。

  C++03之所以让人费神就是因为它没有一个引用类型来绑定到右值,而是用const左值引用来替代,事实证明这个权宜之计并不是长远之道,时隔10年,终归还是要健全引用的左右值语意。

  C++09加入一个新的引用类型——右值引用。右值引用的特点是优先绑定到右值。其语法是&&(注意,不读作“引用的引用”,读作“右值引用”)。有了右值引用,我们前面的方案便可以简单的修改为:

void foo(X const& x);
void foo(X&& x);

  这样一来,左值以及const右值都被绑定到了第一个重载版本。剩下的non-const右值被绑定到第二个重载版本。

  对于你的moveable的类型X,则是这样:

struct X
{
X();
X(X const& o); // copy constructor
X(X&& o); // move constructor
};

X source();
X x = source(); // #1

  在#1处,调用的将会是X::X(X&& o),即所谓的move constructor,因为source()返回的是一个临时对象(non-const右值),重载决议会选中move constructor。

 
 

上一篇:缓冲区溢出原理浅析以及防护  下一篇:瘦身前后——兼谈C++语言进化