C++0x标准出来很长时间了,引入了不少牛逼的特性[1]。其中一个即是右值引用,Thomas Becker的文章[2]很全面的介绍了这个特性,读后有如醍醐灌顶,翻译在此以便深刻理解。php
右值引用是由C++0x标准引入c++的一个使人难以捉摸的特性。我曾偶尔听到过有c++领域的大牛这么说:html
每次我想抓住右值引用的时候,它总能从我手里跑掉。c++
想把右值引用装进脑壳实在太难了。程序员
我不得不教别人右值引用,这太可怕了。算法
右值引用恶心的地方在于,当你看到它的时候根本不知道它的存在有什么意义,它是用来解决什么问题的。因此我不会立刻介绍什么是右值引用。更好的方式是从它将解决的问题入手,而后讲述右值引用是如何解决这些问题的。这样,右值引用的定义才会看起来合理和天然。编程
右值引用至少解决了这两个问题:数组
若是你不懂这两个问题,别担忧,后面会详细地介绍。咱们会从move语义开始,但在开始以前要首先让你回忆起c++的左值和右值是什么。关于左值和右值我很难给出一个严密的定义,不过下面的解释已经足以让你明白什么是左值和右值。函数
在c语言发展的较早时期,左值和右值的定义是这样的:左值是一个能够出如今赋值运算符的左边或者右边的表达式e,而右值则是只能出如今右边的表达式。例如:性能
int a = 42;
int b = 43;
// a与b都是左值
a = b; // ok
b = a; // ok
a = a * b; // ok
// a * b是右值:
int c = a * b; // ok, 右值在等号右边
a * b = 42; // 错误,右值在等号左边优化
在c++中,咱们仍然能够用这个直观的办法来区分左值和右值。不过,c++中的用户自定义类型引入了关于可变性和可赋值性的微妙变化,这会让这个方法变的不那么地正确。咱们没有必要继续深究下去,这里还有另一种定义可让你很好的处理关于右值的问题:左值是一个指向某内存空间的表达式,而且咱们能够用&操做符得到该内存空间的地址。右值就是非左值的表达式。例如:
// 左值:
//
int i = 42;
i = 43; // ok, i是左值
int* p = &i; // ok, i是左值
int& foo();
foo() = 42; // ok, foo()是左值
int* p1 = &foo(); // ok, foo()是左值
// 右值:
//
int foobar();
int j = 0;
j = foobar(); // ok, foobar()是右值
int* p2 = &foobar(); // 错误,不能取右值的地址
j = 42; // ok, 42是右值
若是你对左值和右值的严密的定义有兴趣的话,能够看下Mikael Kilpeläinen的文章[3]。
假设class X包含一个指向某资源的指针或句柄m_pResource
。这里的资源指的是任何须要耗费必定的时间去构造、复制和销毁的东西,好比说以动态数组的形式管理一系列的元素的std::vector
。逻辑上而言X的赋值操做符应该像下面这样:
X& X::operator=(X const & rhs)
{
// [...]
// 销毁m_pResource指向的资源
// 复制rhs.m_pResource所指的资源,并使m_pResource指向它
// [...]
}
一样X的拷贝构造函数也是这样。假设咱们这样来用X:
X foo(); // foo是一个返回值为X的函数
X x;
x = foo();
最后一行有以下的操做:
上面的过程是可行的,可是更有效率的办法是直接交换x和临时对象中的资源指针,而后让临时对象的析构函数去销毁x原来拥有的资源。换句话说,当赋值操做符的右边是右值的时候,咱们但愿赋值操做符被定义成下面这样:
// [...]
// swap m_pResource and rhs.m_pResource
// [...]
这就是所谓的move语义。在以前的c++中,这样的行为是很难实现的。虽然我也听到有的人说他们能够用模版元编程来实现,可是我还历来没有遇到过能给我解释清楚如何具体实现的人。因此这必定是至关复杂的。C++0x经过重载的办法来实现:
X& X::operator=(<mystery type> rhs)
{
// [...]
// swap this->m_pResource and rhs.m_pResource
// [...]
}
既然咱们是要重载赋值运算符,那么<mystery type>
确定是引用类型。另外咱们但愿<mystery type>
具备这样的行为:如今有两种重载,一种参数是普通的引用,另外一种参数是<mystery type>
,那么当参数是个右值时就会选择<mystery type>
,当参数是左值是仍是选择普通的引用类型。
把上面的<mystery type>
换成右值引用,咱们终于看到了右值引用的定义。
若是X是一种类型,那么X&&
就叫作X的右值引用。为了更好的区分两,普通引用如今被称为左值引用。
右值引用和左值引用的行为差很少,可是有几点不一样,最重要的就是函数重载时左值使用左值引用的版本,右值使用右值引用的版本:
void foo(X& x); // 左值引用重载
void foo(X&& x); // 右值引用重载
X x;
X foobar();
foo(x); // 参数是左值,调用foo(X&)
foo(foobar()); // 参数是右值,调用foo(X&&)
重点在于:
右值引用容许函数在编译期根据参数是左值仍是右值来创建分支。
理论上确实能够用这种方式重载任何函数,可是绝大多数状况下这样的重载只出如今拷贝构造函数和赋值运算符中,以用来实现move语义:
X& X::operator=(X const & rhs); // classical implementation
X& X::operator=(X&& rhs)
{
// Move semantics: exchange content between this and rhs
return *this;
}
实现针对右值引用重载的拷贝构造函数与上面相似。
若是你实现了void foo(X&);
,可是没有实现void foo(X&&);
,那么和之前同样foo的参数只能是左值。若是实现了void foo(X const &);
,可是没有实现voidfoo(X&&);
,仍和之前同样,foo的参数既能够是左值也能够是右值。惟一可以区分左值和右值的办法就是实现void foo(X&&);
。最后,若是只实现了实现voidfoo(X&&);
,但却没有实现void foo(X&);
和void foo(X const &);
,那么foo的参数将只能是右值。
c++的初版修正案里有这样一句话:“C++标准委员会不该该制定一条阻止程序员拿起枪朝本身的脚丫子开火的规则。”严肃点说就是c++应该给程序员更多控制的权利,而不是擅自纠正他们的疏忽。因而,按照这种思想,C++0x中既能够在右值上使用move语义,也能够在左值上使用,标准程序库中的函数swap就是一个很好的例子。这里假设X就是前面咱们已经重载右值引用以实现move语义的那个类。
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}
X a, b;
swap(a, b);
上面的代码中没有右值,因此没有使用move语义。但move语义用在这里最合适不过了:当一个变量(a)做为拷贝构造函数或者赋值的来源时,这个变量要么就是之后都不会再使用,要么就是做为赋值操做的目标(a = b)。
C++11中的标准库函数std::move能够解决咱们的问题。这个函数只会作一件事:把它的参数转换为一个右值而且返回。C++11中的swap函数是这样的:
template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
X a, b;
swap(a, b);
如今的swap使用了move语义。值得注意的是对那些没有实现move语义的类型来讲(没有针对右值引用重载拷贝构造函数和赋值操做符),新的swap仍然和旧的同样。
std::move是个很简单的函数,不过如今我还不能将它的实现展示给你,后面再详细说明。
像上面的swap函数同样,尽量的使用std::move会给咱们带来如下好处:
假设有如下代码:
void foo(X&& x)
{
X anotherX = x;
// ...
}
如今考虑一个有趣的问题:在foo函数内,哪一个版本的X拷贝构造函数会被调用呢?这里的x是右值引用类型。把x也看成右值来处理看起来貌似是正确的,也就是调用这个拷贝构造函数:
X(X&& rhs);
有些人可能会认为一个右值引用自己就是右值。但右值引用的设计者们采用了一个更微妙的标准:
右值引用类型既能够被看成左值也能够被看成右值,判断的标准是,若是它有名字,那就是左值,不然就是右值。
在上面的例子中,由于右值引用x是有名字的,因此x被看成左值来处理。
void foo(X&& x)
{
X anotherX = x; // 调用X(X const & rhs)
}
下面是一个没有名字的右值引用被看成右值处理的例子:
X&& goo();
X x = goo(); // 调用X(X&& rhs),goo的返回值没有名字
之因此采用这样的判断方法,是由于:若是容许悄悄地把move语义应用到有名字的东西(好比foo中的x)上面,代码会变得容易出错和让人迷惑。
void foo(X&& x)
{
X anotherX = x;
// x仍然在做用域内
}
这里的x仍然是能够被后面的代码所访问到的,若是把x做为右值看待,那么通过X anotherX = x;
后,x的内容已经发生变化。move语义的重点在于将其应用于那些不重要的东西上面,那些move以后会立刻销毁而不会被再次用到的东西上面。因此就有了上面的准则:若是有名字,那么它就是左值。
那另一半,“若是没有名字,那它就是右值”又如何理解呢?上面goo()的例子中,理论上来讲goo()所引用的对象也可能在X x = goo();
后被访问的到。可是回想一下,这种行为不正是咱们想要的吗?咱们也想为所欲为的在左值上面使用move语义。正是“若是没有名字,那它就是右值”的规则让咱们可以实现强制move语义。其实这就是std::move的原理。这里展现std::move的具体实现仍是太早了点,不过咱们离理解std::move更近了一步。它什么都没作,只是把它的参数经过右值引用的形式传递下去。
std::move(x)
的类型是右值引用,并且它也没有名字,因此它是个右值。所以std::move(x)
正是经过隐藏名字的方式把它的参数变为右值。
下面这个例子将展现记住“若是它有名字”的规则是多么重要。假设你写了一个类Base,而且经过重载拷贝构造函数和赋值操做符实现了move语义:
Base(Base const & rhs); // non-move semantics
Base(Base&& rhs); // move semantics
而后又写了一个继承自Base的类Derived。为了保证Derived对象中的Base部分可以正确实现move语义,必须也重载Derived类的拷贝构造函数和赋值操做符。先让咱们看下拷贝构造函数(赋值操做符的实现相似),左值版本的拷贝构造函数很直白:
Derived(Derived const & rhs)
: Base(rhs)
{
// Derived-specific stuff
}
但右值版本的重载却要仔细研究下,下面是某个不知道“若是它有名字”规则的程序员写的:
Derived(Derived&& rhs)
: Base(rhs) // 错误:rhs是个左值
{
// ...
}
若是像上面这样写,调用的永远是Base的非move语义的拷贝构造函数。由于rhs有名字,因此它是个左值。但咱们想要调用的倒是move语义的拷贝构造函数,因此应该这么写:
Derived(Derived&& rhs)
: Base(std::move(rhs)) // good, calls Base(Base&& rhs)
{
// Derived-specific stuff
}
如今有这么一个函数:
X foo()
{
X x;
// perhaps do something to x
return x;
}
一看到这个函数,你可能会说,咦,这个函数里有一个复制的动做,不如让它使用move语义:
X foo()
{
X x;
// perhaps do something to x
return std::move(x); // making it worse!
}
很不幸的是,这样不但没有帮助反而会让它变的更糟。如今的编译器基本上都会作返回值优化(return value optimization)。也就是说,编译器会在函数返回的地方直接建立对象,而不是在函数中建立后再复制出来。很明显,这比move语义还要好一点。
因此,为了更好的使用右值引用和move语义,你得很好的理解如今编译器的一些特殊效果,好比return value optimization和copy elision。而且在运用右值引用和move语义时将其考虑在内。Dave Abrahams就这一主题写了一系列的文章[4]。
除了实现move语义以外,右值引用要解决的另外一个问题就是完美转发问题(perfect forwarding)。假设有下面这样一个工厂函数:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg arg)
{
return shared_ptr<T>(new T(arg));
}
很明显,这个函数的意图是想把参数arg转发给T的构造函数。对参数arg而言,理想的状况是好像factory函数不存在同样,直接调用构造函数,这就是所谓的“完美转发”。但真实状况是这个函数是错误的,由于它引入了额外的经过值的函数调用,这将不适用于那些以引用为参数的构造函数。
最多见的解决方法,好比被boost::bind采用的,就是让外面的函数以引用做为参数。
template<typename T, typename Arg>
shared_ptr<T> factory(Arg& arg)
{
return shared_ptr<T>(new T(arg));
}
这样确实会好一点,但不是完美的。如今的问题是这个函数不能接受右值做为参数:
factory<X>(hoo()); // error if hoo returns by value
factory<X>(41); // error
这个问题能够经过一个接受const引用的重载解决:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg const & arg)
{
return shared_ptr<T>(new T(arg));
}
这个办法仍然有两个问题。首先若是factory函数的参数不是一个而是多个,那就须要针对每一个参数都要写const引用和non-const引用的重载。代码会变的出奇的长。
其次这种办法也称不上是完美转发,由于它不能实现move语义。factory内的构造函数的参数是个左值(由于它有名字),因此即便构造函数自己已经支持,factory也没法实现move语义。
右值引用能够很好的解决上面这些问题。它使得不经过重载而实现真正的完美转发成为可能。为了弄清楚是如何实现的,咱们还须要再掌握两个右值引用的规则。
第一条右值引用的规则也会影响到左值引用。回想一下,在c++11标准以前,是不容许出现对某个引用的引用的:像A& &
这样的语句会致使编译错误。不一样的是,在c++11标准里面引入了引用叠加规则:
A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&
另一个是模版参数推导规则。这里的模版是接受一个右值引用做为模版参数的函数模版。
template<typename T>
void foo(T&&);
针对这样的模版有以下的规则:
有了上面这些规则,咱们能够用右值引用来解决前面的完美转发问题。下面是解决的办法:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
std::forward的定义以下:
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
上面的程序是如何解决完美转发的问题的?咱们须要讨论当factory的参数是左值或右值这两种状况。假设A和X是两种类型。先来看factory<A>
的参数是X类型的左值时的状况:
X x;
factory<A>(x);
根据上面的规则能够推导获得,factory的模版参数Arg变成了X&,因而编译器会像下面这样将模版实例化:
shared_ptr<A> factory(X& && arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& && forward(remove_reference<X&>::type& a) noexcept
{
return static_cast<X& &&>(a);
}
应用前面的引用叠加规则而且求得remove_reference的值后,上面的代码又变成了这样:
shared_ptr<A> factory(X& arg)
{
return shared_ptr<A>(new A(std::forward<X&>(arg)));
}
X& std::forward(X& a)
{
return static_cast<X&>(a);
}
这对于左值来讲固然是完美转发:经过两次中转,参数arg被传递给了A的构造函数,这两次中转都是经过左值引用完成的。
如今再考虑参数是右值的状况:
X foo();
factory<A>(foo());
再次根据上面的规则推导获得:
shared_ptr<A> factory(X&& arg)
{
return shared_ptr<A>(new A(std::forward<X>(arg)));
}
X&& forward(X& a) noexcept
{
return static_cast<X&&>(a);
}
对右值来讲,这也是完美转发:参数经过两次中转被传递给A的构造函数。另外对A的构造函数来讲,它的参数是个被声明为右值引用类型的表达式,而且它尚未名字。那么根据第5节中的规则能够判断,它就是个右值。这意味着这样的转发无缺的保留了move语义,就像factory函数并不存在同样。
事实上std::forward的真正目的在于保留move语义。若是没有std::forward,一切都是正常的,但有一点除外:A的构造函数的参数是有名字的,那这个参数就只能是个左值。
若是你想再深刻挖掘一点的话,不妨问下本身这个问题:为何须要remove_reference?答案是其实根本不须要。若是把remove_reference<S>::type&
换成S&
,同样能够得出和上面相同的结论。可是这一切的前提是咱们指定Arg做为std::forward的模版参数。remove_reference存在的缘由就是强迫咱们去这样作。
已经讲的差很少了,剩下的就是std::move的实现了。记住,std::move的用意在于将它的参数传递下去,将它转换成右值。
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
下面假设咱们针对一个X类型的左值调用std::move。
X x;
std::move(x);
根据前面的模版参数推导规则,模版参数T变成了X&,因而:
typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{
typedef typename remove_reference<X&>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
而后求得remove_reference的值,并应用引用叠加规则,获得:
X&& std::move(X& a) noexcept
{
return static_cast<X&&>(a);
}
这就能够了,x变成了没有名字的右值引用。
参数是右值的状况由你来本身推导。不过你可能立刻就想跳过去了,为何会有人把std::move用在右值上呢?它的功能不就是把参数变成右值么。另外你可能也注意到了,咱们彻底能够用static_cast<X&&>(x)
来代替std::move(x)
,不过大多数状况下仍是用std::move(x)
比较好。