C++中的右值引用

来源:互联网 发布:软件注册码 编辑:程序博客网 时间:2024/05/16 00:37

1.简介

右值引用是加入到C++11标准的一个新特征。右值引用令人难以理解的地方在于当你第一眼看到它时,你根本不知道它的目的或者它想解决什么问题。所以,我现在并不会直接解释什么是右值引用,相反,我会从一些需要解决的问题开始,并解释右值引用是如何解决这个问题的。通过这种方式,右值引用的定义将会变得更加通俗易懂,更加自然一点。

右值引用至少解决下面两个问题:
1. 实现移动语义
2. 完美转发

如果你对上面这两种问题都不熟悉,不要担心,下面我会详细解释这两个问题。我们将从移动语义开始,但是在我们开始前,我需要向你们解释一下C++中的左值和右值。给出一个严格的定义极其困难,但是下面的解释对于目前来说足够了。

最原始的来自于早期的C语言对于左值和右值的定义如下:左值是一种可以出现在赋值语句的左边或者右边的表达式,而右值是只能出现在赋值语句右边的表达式。举个例子,

int a = 42;int b = 43;// a and b are both l-values:a = b; // okb = a; // oka = a * b;// ok// a*b is an rvalue:int c = a * b; // ok, rvalue on right hand side of assignmenta * b = 42; // error, rvalue on left hand side of assignment

C++中,这种定义对于第一眼直观的区分左值和右值很有用。然而,C++中用户自定义类型引入了一些关于可修改性和可赋值性的子问题,而这些让上面的定义变得不再正确,对此我们没有必须再深入进去(译者注:感兴趣的可以继续深入)。这里有另外一个版本的定义可以帮助你识别右值引用,尽管该定义还存在一些争论。左值是这样一种表达式,它指向一块物理内存,并允许我们通过运算符&来取得这块内存的地址,而右值则是非左值的表达式。举个例子,

// lvalues//int i = 42;i = 43; // ok, i is an lvalueint* p = &i; // ok, i is an lvalueint& foo();foo() = 42; // ok, foo() is an lvalue(译者注:这里其实是指foo函数返回的对象是一个左值)int* p1 = &foo(); // ok, foo() is an lvalue//rvalue//int foobar();int j = 0;j = foobar(); //ok, foobar() is an rvalue(译者注:理由同上,返回值是一个临时变量,是一个右值)int* p2 = &fooBar(); // error, cannot take the address of an rvaluej = 42; // ok, 42 is an rvalue

如果你对于左值和右值的严格定义感兴趣,一个开始的好地方是这里


2.移动语义

假设X是一个类,其包含有一个指向某种资源的指针或者句柄,就叫m_pResource。这里说的资源是一种需要花费精力去构造、复制和析构的东西,一个好例子就是std::vector,它分配一组内存来保存一组对象集合。逻辑上来说,对于X的复制赋值(Copy Assignment)操作看起来就像这样:

X& X:operator=(X const& rhs){    // [...]    // Make a clone of what rhs.m_pResource refers to    // Destruct the resource that m_pResource refers to    // Attach the clone to m_pResource    // [...]}

对于复制构造(Copy Constructor)也是同理。现在假设X被使用如下:

X foo();X x;//perhaps use x in various waysx = foo()

上面的最后一行:
1. 复制从foo函数返回的临时对象所持有的资源
2. 对X本身所持有的资源进行析构,然后替换成复制的资源
3. 对临时对象进行析构,当然包括对于其所持有殴打资源进行析构

很显然,如果我们将X和临时对象的资源指针(句柄)进行互换,然后让临时对象的析构函数析构掉X的原始资源,这样一种方式肯定是可以的,并且更加有效率。换句话说,在赋值语句的右端是一个右值的特殊情况下,我们希望复制赋值的操作如下:

// [...]// swap m_pResource and rhs.m_pResource// [...]

这就叫移动语义。在C++11中,这种有条件行为可以通过重载来实现:

X& X::operator=(<mystery type> rhs){    // [...]    // swap this->m_pResource and rhs.m_pResource    // [...]}

考虑到我们正在定义复制赋值的操作符重载,我们的”mystery type“必须是一种引用类型:我们肯定希望以引用方式来传递rhs。另外,我们期待mystery type具有以下行为:当我们需要在两个重载函数中做选择时,一个是正常的引用类型,另外一个mystery type,那么右值必须选择mystery type,左值必须选择正常引用类型。

如果你现在对于上面的选择也是对于右值选择mystery type,那么你其实正在看对于右值引用的定义。


3.右值引用

如果X是任意类型,那么X&&就叫做X的右值引用。为了更好的区分,正常引用也被称为左值引用。

右值引用是一种行为非常像左值引用的类型,但是具有一些例外,其中最重要的一点就表现在函数重载决议。左值更喜欢旧风格的左值引用,而右值则更偏爱新的右值引用:

void foo(X& x); // lvalue reference overloadvoid foo(X&& x): // rvalue reference overloadX x;X foobar();foo(x); // argument is lvalue: call foo(X&)foo(foobar()); // argument is rvalue:call foo(X&&)

所以它的要点就是:

右值引用允许一个函数在编译期间根据“我是以一个左值被调用的还是一个右值”决定分支(通过重载决议)

你可以按照这种方式来重载任意函数,就像上面展示的一样。但是在绝大多数情况下,这种方式的重载应该只应用在复制构造函数和赋值操作符上,目的是为了实现移动语义:

X& X::operator=(X const & rhs); // classical implemetationX& X::operator=(X&& rhs){    // Move semantics: exchange content between this and rhs    return *this;}

对复制构造函数实现右值引用重载跟上面同理。

警告:正如在C++中经常会发生的,第一眼看起来对的很多都不够完美。事实证明在一些情况下,对于上面复制赋值操作符的重载函数中的对于this和rhs的内容交换最简单的实现也不够好。我们会在下面的第4章节回顾这个话题

注解:
如果你实现 void foo(X&) 但没有实现 void foo(X&&),那么行为肯定是不可变的:foo只能调用在左值上,而不能调用在右值上。
如果你实现void foo(X const &)但没有实现void foo(X&&),那么行为肯定是不可变的:foo可以调用在左值和右值上,但这个却没法区分是左值调用还是右值调用。如果要区分,那么只能通过也实现void foo(X&&)
如果你实现了void foo(X&&),但是却没有实现void foo(X&)foo(X const &)中任意一个,那么根据C++11的最终版本,foo只能调用在右值上,任何调用在左值上的尝试都会触发编译错误。


4. 强制移动语义

众所周知,对于C++标准的第一个修正案指出:

The committee shall make no rule that prevents C++ programmers from shooting themselves in the foot.

通俗的说,就是当面临选择是给程序员更多控制还是保护他们避免由于自己的粗心导致的错误,C++更倾向于给程序员更多的控制。秉持这项原则,C++11允许你不仅可以使用移动语义在右值上,还可以按照你的意愿应用在左值上。一个好的例子就是标准库里面的函数swap。跟以前一样,让X表示一个已经重载了复制构造函数和复制赋值操作符的函数。

template<class T>void swap(T& a, T& b){    T tmp(a);    a = b;    b = temp;}X a,b;swap(a,b);

这里没有任何右值。所以,swap函数里面那三行使用的都是非移动语义。但是我们知道使用移动语义其实会更好:每当一个变量作为一个复制构造函数或者赋值操作的源时,这个变量要么就再也没有使用过,要么就是只作为赋值语句的目标。

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);}

现在swap函数里面那三行全都是用了移动语义。记住对于没有实现移动语义(就是没有重载它们的带有右值引用版本的复制构造函数和赋值运算符)的类型,新的swap函数和旧的版本表现的是一样的。

std::move是一个非常简单的函数。不幸的是,我现在还不能向你展示其实现细节。我们后面会回顾整个话题。

像上面展示的那样,在任何可以使用的地方使用std::move会给我们带来如下的好处:
- 对于那些实现了移动语义的类型,许多标准算法和操作将使用移动语义,并将可能产生巨大的性能收益。一个重要的例子就是原地排序:原地排序算法基本上就是做元素互换,而这种排序算法将利用移动语义对所有提供了移动语义的类型进行排序。
- STL经常要求特定类型的可拷贝性,例如,可作为容器元素的类型。深入观察后,其实对于很多情况,可移动性就足够了。所以,我们现在可以在很多以前不可以使用的地方使用具有可移动性但不具有可拷贝性的类型(想到了unique_pointer)。例如,这些类型现在可以作为STL容器元素的类型。

现在我们知道了std:move,我们想明白为什么之前我在第三节警告中说实现复制赋值操作符的右值引用版本的重载函数有一些问题。考虑一个简单的变量间赋值,像这样:

a = b;

你期待这里会发生什么?你期待被a持有的对象被b的复制所替换,基于这种替换,你还期待之前被a所持有的对象被析构了。现在考虑下面这一行代码:

a = std::move(b);

如果移动语义被实现成一个简单的交换,那么上面代码的影响就是将a和b所持有的对象进行了互换,并没有任何对象被析构。当然,最后a之前所持有的对象还是会被析构,即当代码离开了b的生存域。除非,当b又作为参数传递给了新的函数。所以,只要考虑到复制赋值操作符的实现,我们就没法清除什么时候a之前所持有的对象会被析构。

某种层次上,我们已经进入了非确定性析构的阴暗世界:一个变量已经被赋值,但是其之前所持有的对象还依旧在某处存在。其实只有那个对象的析构不会对外界的世界产生什么可见的副作用,就没什么关系。一个反例就是在再析构函数中释放锁。所以,一个对象的析构函数中任意有副作用的部分都应该显式地在重载的赋值操作符中执行:

X& X::operator=(X&& rhs){    // Perform a cleanup that take care of at least those parts of the    // destruction that have side effects.Be sure to leave the object    // in a destructable and assignable state    // Move semantics: exchange content between this and rhs    return *this;}

5.右值引用是右值吗?

和之前一样,让X表示一个已经重载了复制构造和赋值操作符的来实现移动语义的类,现在考虑:

void foo(X&& x){    X anotherX = x;}

有趣的问题是:在foo的内部将调用哪一个版本的复制构造函数?这里,x是一个声明为右值引用的变量,那就是说,一个更倾向于并且基本上(但不是必须)指向一个右值的引用。所以,我们推断x自身也是像一个右值一样进行绑定,即:

X(X&& rhs)

应该被调用。换句话说,我们可以期待任何一个被声明为右值引用的变量都是右值。右值引用的设计者采取了一种更加微妙的解决方案:

Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

在上面的例子中,声明为右值引用的变量有一个名字,所以它是一个左值:

void foo(X&& x){    X anotherX = x; // calls X(X const & rhs)}

下面是一个声明为左值引用但是没有名字的例子,所以它是一个右值:

X&& goo();X x = goo(); // calls 

这种设计背后的原理就是:默认允许移动语义应用到拥有名字的变量,比如:

X anotherX = x;// x is still in scope

会非常迷惑并且易于出错,因为我们刚刚移动过的变量,或者说刚刚剽窃过的变量,还是可以在随后代码中进行访问。但是移动语义的重点在于只有在它不影响的地方才去应用它,这种不影响就是我们移动过的变量会死亡并且在移动完成后立刻消失,所以有这条规则:

if it has a name, then it’s an lvale

那么对于另一部分

if it does not have a name, then it’s an rvalue

怎么理解呢?看一下上面的goo的例子,技术上可行但不是很有可能的一点就是第二行的goo()表达式表示某种在移动操作完成后依旧可以访问的东西。但是回想我们之前的章节:something that’s what we want!我们想要能够按照我们的意愿强制对左值进行移动语义操作,而规则,“if it does not have a name, then it’s an rvalue”允许我们以一种可控制的方式实现它。这就是函数std::move的工作方式。尽管现在展示其实现还为时尚早,我们对于理解std::move又更近了一步。它通过引用方式传入参数,什么都没有做,然后返回类型是右值引用。所以表达式

std::move(x)

声明为右值引用但是没有一个名字,所以它就是一个右值。因此,std::move将传入的参数转换成右值,即使传入的参数并不是右值,它实现的方式就是通过隐藏名字

下面是向你展示如上规则是多么重要的一个例子。假设你已经写了一个类Base,并且你已经通过重载Base的复制构造函数和赋值操作符来实现其移动语义。

Base(Base const & rhs); // non-move semanticsBase(Base&& rhs); // move semantics

现在你写了一个继承Base的类Derived.为了保证Derived对象的Base部分应用了移动语义,你必须也要重载Derived的复制构造和复制赋值操作符。我们看一下复制构造。复制赋值操作符按照类似的方式进行处理。左值的版本非常直观:

Derived(Derived const &rhs)    : Base(rhs){    // Derived-specific stuff}

右值的版本就非常微妙了。下面是一些没有意识上以上规则的人可能会写出来的:

Derived(Derived&& rhs)    :Base(rhs) // wrong: rhs is an lvalue{    // Derived-specific stuff}

如果我们像上面那样写,那么Base的非移动语义版本的复制构造函数会被调用,因为rhs有一个名字,所以是一个左值。我们想要被调用的是Base的移动复制构造函数,正确的方式是这样写:

Derived(Derived&& rhs)    :Base(std::move(rhs)){    // Derived-specific stuff        }

6.移动语义和编译优化

考虑下面的函数定义:

X foo(){    X x;    //perhaps do something to x    return x;}

现在假设和之前一样,X是一个已经实现移动复制构造函数和移动赋值操作符重载的类。如果你对于上面函数定义的看法停留在表面,你可能会被诱惑着说,等一下,有一个值的复制发生在xfoo的返回值之间。而如果我们使用了移动语义操作:

X foo(){    X x;    //perhaps do something to x    return std::move(x);// making it worse!}

不幸的是,这只会让事情变得更糟。任何一个现代编译器都会对原始的那个函数定义应用返回值优化。换句话说,编译器会直接在foo返回值的位置构造一个X对象,而不是在内部构造一个X对象然后将它复制出去。显而易见,这种方式甚至比移动语义还要好。

所以正如你所看到的,为了能够真正以最优的方式使用右值引用和移动语义,你需要全面理解并且将编译器的”特殊效果”考虑进去。更详细的讨论可以参考Scott Meyers的著作“Effective Modern C++”。这真的会很微妙,难以捉摸,但是嘿,我们选择C++作为我们的语言肯定是为了某个理由,对吧?我们既然做了我们的床,现在我们做的就是躺上去。


7.完美转发:问题

另一个可以移动语义进行解决的问题就是完美转发问题。考虑下面的简单factory函数:

template<typename T, typename Arg>shared_ptr<T> factory(Arg arg){    return shared_ptr<T>(new T(arg));}

显然,这里的目的就是将参数argfactory函数转发给T的构造函数。理想情况下,只考虑参数arg,客户端代码所有都应该表现正常,就像这个factory函数不存在而直接调用构造函数一样:完美转发。上面的代码在这种情况下会错误惨烈:它引入了一个额外的按值调用,而如果构造函数的参数是引用的话,这个将会极其糟糕。

最常见的解决方案,比如boost::bind,就是让外面的函数也采取引用作为参数。

template<typename T, typename Arg>shared_ptr<T> factory(Arg& arg){    return shared_ptr<T>(new T(arg));}

这样比之前好,但不是完美的。问题在于现在factory函数不能调用在右值上了:

factory<X>(hoo()); // error if hoo returns by valuefactory<X>(41);// error

这个可以通过提供一个接受常引用参数的重载函数来解决:

template<typename T, typename Arg> shared_ptr<T> factory(Arg const & arg){   return shared_ptr<T>(new T(arg));} 

但这种方式存在两个问题。第一,如果factory函数有不止一个,而是好几个参数,你将需要提供所有非常引用和常引用参数组合的重载函数。所以,这种方案对于有一些参数的函数来说可扩展性非常差。

第二,这种方式的转发还达不到完美,因为它将移动语义拒之门外:内部的构造函数的参数是一个左值。所以,移动语义永远也不会发生,即使将外面的函数去掉也不行。

事实证明,右值引用可以用来解决上面两个问题。它们让实现完美转发成为可能,而不用使用到重载。为了明白这一切是如何发生的,我们需要再看两条关于右值引用的规则。


8.完美转发:解决方案

剩余两条关于右值引用的规则中的第一条也对旧风格的左值引用有影响。回想在pre-11 C++中,是不允许使用引用的引用的:一些看起来像是A& &将会导致编译错误。相比之下,在C++11中,引入了下面的reference collapsing rules
- A& & becomes A&
- A& && becomes A&
- A&& & becomes A&
- A&& && becomes A&&
第二条规则,对于函数模板,有一个特殊的模板参数推理规则能够将模板参数以右值引用的方式接受:

template<typename T>void foo(T&&);

这里,下面可以应用:
1. 当foo以参数为A类型的左值被调用时,那么T就解析成A&,所以根据上面的reference collapsing rules,参数类型就变成A&
2. 当foo以参数为A类型的右值被调用时,那么T就解析成A,那么参数就变成A&&

基于以上规则,我们现在可以使用右值引用来解决完美转发问题了。下面是解决方案的大概实现:

template<typename T, typename Arg>shared_ptr<T> factory(Arg&& arg){    return shared_ptr<T>(new T(std::forward<Arg>(arg)));}template<class S>S&& forward(typename remove_reference<S>::type& a) noexcept{    return static_cast<S&&>(a);}

(noexcept会在章节9说到,现在不必过多关心)为了弄明白以上代码是如何实现完美转发的,我们将分别讨论当我们的factory函数在接收到左值和右值时会发生什么。让AX表示类型,首先假设factory以类型X的左值为参数进行调用:

X x;factory<A>(x);

那么,根据特殊模板推断规则,factory的模板参数Arg被解析成X&。所以,编译器会生成如下的factory函数实例和std::forward实例:

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);}

这肯定就是左值的完美转发啊:factory函数的参数arg通过两层间接转发到A的构造函数中,而这两层的参数都是通过旧风格的左值引用。

下面,我们假设factory是通过X类型的右值进行调用的:

X foo();factory<A>(foo());

那么,再次根据特殊模板推断规则,factory函数的模板参数被解析成X。所以,编译器会生成如下的factory函数实例和std::forward实例:

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);}

而这正又是右值的完美转发:factory函数的参数通过两层间接转发到A的构造函数中,而这两层的参数都是通过引用。另外,A的构造函数看到它的参数是一个声明为右值引用的表达式并且该表达式没有一个名字,根据no-name rule,这样的东西是一个右值。所以,A的构造函数是以右值为参数进行调用的。这就意味着即使外面的factory函数不在了,转发也能保持原有的移动语义不变。

值得一提的是,保持移动语义是std::forward存在的唯一目的。没有了std::forward,一切还是会正常工作,除了一点就是A的构造函数看到的参数总是具有一个名字,而这是一个左值。

如果你想研究的更深一点,你可以问自己这个问题:为什么在std::forward的定义中需要remove_reference?答案就是,它并不是必须的。如果你在定义中使用S&来替代remove_reference::type&,你可以完全重现之前的一切,让你确定完美转发依旧工作正常。然而,只有当你明确指定Arg作为std::forward模板参数的时候,它才会正常工作。而remove_reference在这里存在的目的就是强制我们这么做。(译者注:这里如果使用remove_referece,假设你给std::forward的模板参数不小心写成了Arg&之类的,通过remove_reference可以将引用去除,然后加上&依旧使得最终的参数类型是Arg&

开心!我们已经差不多快完结了。现在我们只剩下去看一下std::move的实现了。记得吗,std::move的目的就是将通过引用传递的参数传递进去,然后表现的像个右值。下面是其实现:

template<class T>typename remove_reference<T>::typename&&std::move(T&& a) noexcept{    typedef typename remove_reference<T>::type&& RvalRef;    return static_cast<RvalRef>(a);}

假设我们以左值为参数来调用std::move

X x;std::move(x);

根据上面的特殊模板推断规则,模板参数T会解析成X&,所以,编译器最终实例化的样子就是:

typename remove_reference<X&>::typename&&std::move(X& && a) noexcept{    typedef typename remove_reference<X&>::type&& RvalRef;    return static_cast<RvalRef>(a);}

在完成了remove_reference和应用了新的reference collapsing rules,这就变成:

X&& std::move(X& a) noexcept{    return static_cast<X&&>(a);}

这就完成了工作:我们的左值x会被绑定到参数类型的左值引用,然后这个函数将其传递进去,然后将其转换成一个没有名字的右值引用。

我将验证std::move在接收右值也可以正常工作交给读者。但是你可能想跳过这个:为什么还有人想用右值为参数调用std::move,而其目的居然是为了将其转换成右值?另外,你或许已经注意到了除了可以写

std::move(x);

你也可以这样写:

static_cast<X&&>(x);

但是,一般强烈建议使用std::move,因为这样更加直观!


9.右值引用和异常

一般来说,当你用C++开发程序时,你会面临如下选择:你是想注重异常安全,还是想在你的代码中使用异常(处理)。右值在这一点上有一点不同。当你为了实现移动语义而重载一个类的复制构造和复制赋值运算符时,非常建议你做下面的:
1. 力争让你的重载函数没有任何异常抛出。这通常非常微不足道,因为移动语义一般就做两个对象间的指针和资源句柄的交换。
2. 如果你已经成功地使得你的重载函数没有任何异常,那么确保使用关键字noexcept来提示这一事实。

如果你没有做这两件事,那么至少会存在一个常见场景下,你的移动语义不会生效,即便你非常地期待它会生效:当一个std::vectorresized的时候,你当然希望当vcetor里面现存的元素要relocated到新的内存块时,移动语义会发生。但是除非同时满足了上面的1和2,否则不会发生。

你不必真的弄明白这其中的原因。仔细查看推荐1和2已经够简单的了,而这也是你全部需要知道的。然而,如果你想深入理解这其中的原因,推荐阅读Scott Meyers的著作”Effective Modern C++”的Item 14;


10.The Case of the Implicit Move

At one point during the (often complex and controversial) discussion of rvalue references, the Standard Committee decided that move constructors and move assignment operators, that is, rvalue reference overloads for copy constructors and copy assignment operators, should be generated by the compiler when not provided by the user. This seems like a natural and plausible thing to request, considering that the compiler has always done the exact same thing for ordinary copy constructors and copy assignment operators. In August of 2010, Scott Meyers posted a message on comp.lang.c++ in which he explained how compiler-generated move constructors can break existing code in a rather serious way.
The committee then decided that this was indeed cause for alarm, and it restricted the automatic generation of move constructors and move assignment operators in such a way that it is much less likely, though not impossible, for existing code to break. The final state of affairs is described in detail in Item 17 of Scott Meyers’ book “Effective Modern C++.”

The issue of implicitly moving remained controversial all the way up to the finalization of the Standard (see e.g. this paper by Dave Abrahams). In an ironic twist of fate, the only reason why the committee considered implicit move in the first place was as an attempt to resolve the issue with rvalue references and exceptions as mentioned in Section 9. This problem was subsequently solved in a more satisfactory way via the new noexcept keyword. Had the noexcept solution been found just a few months earlier, implicit move may have never seen the light of day. Oh well, so it goes.

Ok, that’s it, the whole story on rvalue references. As you can see, the benefits are considerable. The details are gory. As a C++ professional, you will have to understand these details. Otherwise, you have given up on fully understanding the central tool of your trade. You can take solace, though, in the thought that in your day-to-day programming, you will only have to remember three things about rvalue references:
1. By overloading a function like this:

void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload

you can branch at compile time on the condition “is foo being called on an lvalue or an rvalue?” The primary (and for all practical purposes, the only) application of that is to overload the copy constructor and copy assignment operator of a class for the sake of implementing move semantics. If and when you do that, make sure to pay attention to exception handling, and use the new noexcept keyword as much as you can.
2. std::move turns its argument into an rvalue.
3. std::forward allows you to achieve perfect forwarding if you use it exactly as shown in the factory function example in Section 8.


11.Acknowledgments and Further Reading

I am indebted to Thomas Witt for the insight and information on rvalue references that he shared with me. Thanks to Valentin David for reading the article carefully and providing valuable corrections and insights. Many more readers have helped to improve the article. Thanks everybody for their contributions, and please do keep sending feedback. All remaining inaccuracies and deficiencies are mine.
I very much recommend reading the article by Howard E. Hinnant, Bjarne Stroustrup, and Bronek Kozicki on the subject of C++ rvalue references at the C++ Source. This article has more and better examples than mine does, and it has an extensive list of links to proposals and technical papers which you will find interesting. As a tradeoff, the article does not go into all the details the way I do; for example, it does not explicitly state the new reference collapsing rules or the special template argument deduction rule for rvalue references.

Finally, your primary source of reference for all things C++11 and C++14 should be Scott Meyers’ book “Effective Modern C++.” While my presentation in this article gave you sort of a sharp focus on rvalue references, Scott Meyers’ book paints the entire picture of modern C++, whithout ever cutting corners on details.

原文链接

原创粉丝点击