深入右值引用,move语义和完美转发

深入右值引用,move语义和完美转发

转载请注明:http://blog.csdn.net/booirror/article/details/45057689

乍看起来,move语义使得你可以用廉价的move赋值替代昂贵的copy赋值,完美转发使得你可以将传来的任意参数转发给 其他函数,而右值引用使得move语义和完美转发成为可能。然而,慢慢地你发现这不那么简单,你发现std::move并没有move任何东西,完美转发也并不完美,而T&&也不一定就是右值引用……

move语义

最原始的左值和右值定义可以追溯到C语言时代,左值是可以出现在赋值符的左边和右边,然而右值只能出现在赋值符的右边。在C++里,这种方法作为初步判断左值或右值还是可以的,但不只是那么准确了。你要说C++中的右值到底是什么,这真的很难给出一个确切的定义。你可以对某个值进行取地址运算,如果不能得到地址,那么可以认为这是个右值。例如:

int& foo();
foo() = 3; //ok, foo() is an lvalue

int bar();
int a = bar(); // ok, bar() is an rvalue

为什么要move语义呢?它可以让你写出更高效的代码。看下面代码:

string foo();
string name("jack");
name = foo();

第三句赋值会调用string的赋值操作符函数,发生了以下事情:

  1. 首先要销毁name的字符串吧
  2. 把foo()返回的临时字符串拷贝到name吧
  3. 最后还要销毁foo()返回的临时字符串吧

这就显得很不高效,在C++11之前,你要些的高效点,可以是swap交换资源。C++11的move语义就是要做这事,这时重载move赋值操作符

string& string::operator=(string&& rhs);

move语义不仅仅用于右值,也用于左值。标准库提供了std::move方法,将左值转换成右值。因此,对于swap函数,我们可以这样实现:

template<class T>
void swap(T& a, T& b)
{
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}

右值引用

string&& 这个类型就是所谓的右值引用,而把T&称之为左值引用。注意,不要见到T&&就认为是右值引用,例如,下面这个就不是右值引用:

T&& foo = T(); //右值引用
auto&& bar = foo; // 不是右值引用

实际上,T&&有两种含义,一种就是常见的右值引用;另一种是即可以是右值引用,也可以是左值引用,Scott Meyers把这种称为Universal Reference,后来C++委员把这个改成forwarding
reference
,毕竟forwarding reference只在某些特定上下文才出现。

有了右值引用,C++11增加了move构造和move赋值。考虑这个情况:

void foo(X&& x)
{
  // ...
}

那么问题来了,x的类型是右值引用,指向一个右值,但x本身是左值还是右值呢?C++11对此做出了区分:

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.

由此可知,x是个左值。考虑到派生类的move构造,我们因这样写才正确:

Derived(Derived&& rhs):Base(std::move(rhs) //std::move不可缺
{ ... }

有一点必须明白,那就是std::move不管接受的参数是lvalue,还是rvalue都返回rvalue。因此我们可以给出std::move的实现如下(很接近于标准实现):

template <class T>
typename remove_reference<T>::type&& move(T&& t)
{
    using RRefType = typename remove_reference<T>::type&&;
    return static_cast<RRefType>(t);
}

完美转发

假设有一个函数foo,我们写出如下函数,把接受到的参数转发给foo:

template<class T>
void fwd(TYPE t)
{
    foo(t);
}

我们一个个来分析:

  • 如果TYPE是T的话,假设foo的参数引用类型,我会修改传进来的参数,那么fwd(t)和foo(t)将导致不一样的效果。
  • 如果TYPE是T&的话,那么fwd传一个右值进来,没法接受,编译出错。
  • 如果TYPE是T&,而且重载个const T&来接受右值,看似可以,但如果多个参数呢,你得来个排列组合的重载,因此是不通用的做法。

你很难找到一个好方法来实现它,右值引用的引入解决了这个问题,在这种上下文时,它成为forwarding reference。 这就涉及到两条原则。第一条原则是引用折叠原则:

  • A& & 折叠成 A&
  • A& && 折叠成 A&
  • A&& & 折叠成 A&
  • A&& && 折叠成 A&&

第二条是特殊模板参数推导原则:

1.如果fwd传进的是个A类型的左值,那么T被决议为A&。 2.如果fwd传进的是个A类型的右值,那么T被决议为A。

将两条原则结合起来,就可以实现完美转发。

A x;
fwd(x); //推导出fwd(A& &&) 折叠后fwd(A&)

A foo();
fwd(foo());//推导出fwd(A&& &&) 折叠后 fwd(A&&)

std::forward应用于forwarding reference,代码看起来如下:

template<class T>
void fwd(T&& t)
{
    foo(std::forward<T>(t));
}

要想展开完美转发的过程,我们必须写出forward的实现。接下来就尝试forward该如何实现,分析一下,std::forward是条件cast的,T的推导类型取决于传参给t的是左值还是右值。因此,forward需要做的事情就是当且仅当右值传给t时,也就是当T推导为非引用类型时,forward需要将t(左值)转成右值。forward可以如下实现:

template<class T>
T&& forward(typename remove_reference<T>::type& t)
{
    return static_cast<T&&>(t);
}

现在来看看完美转发是怎么工作的,我们预期当传进fwd的参数是左值,从forward返回的是左值引用;传进的是右值,forward返回的是右值引用。假设传给fwd是A类型的左值,那么T被推导为A&:

void fwd(A& && t)
{
    foo(std::forward<A&>(t));
}

forward<A&>实例化:

A& && forward(typename remove_reference<A&>::type& t)
{
    return static_cast<A& &&>(t);
}

引用折叠后:

A& forward(A& t)
{
    return static_cast<A&>(t);
}

可见,符合预期。再看看传入fwd是右值时,那么T被推导为A:

void fwd(A && t)
{
    foo(std::forward<A>(t));
}

forward<A>实例化如下:

A&& forward(typename remove_reference<A>::type& t)
{
    return static_cast<A&&>(t);
}

也就是:

A&& forward(A& t)
{
    return static_cast<A&&>(t);
}

forward返回右值引用,很好,完全符合预期。

总结

C++11之前,auto_ptr不能放入容器中,C++11的move语义解决了这个问题,unique_ptr就是auto_ptr的替代版。如果声明个指向引用的引用类型的变量,比如你写出如下代码:

int a = 3;
auto & & b = a;

这是不合法,编译器会报错。再看看完美转发:

void f(vector<int> vi;
f({1,2,3});//ok
fwd({1,2,3})//error

还有些其他情况,你需要明白,完美转发也不完美。

时间: 2024-10-27 05:14:34

深入右值引用,move语义和完美转发的相关文章

右值引用,转移语义与完美转发

1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& foo()

详解C++右值引用

http://jxq.me/2012/06/06/%E8%AF%91%E8%AF%A6%E8%A7%A3c%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/#thbecker C++0x标准出来很长时间了,引入了很多牛逼的特性[1].其中一个便是右值引用,Thomas Becker的文章[2]很全面的介绍了这个特性,读后有如醍醐灌顶,翻译在此以便深入理解. 目录 概述 move语义 右值引用 强制move语义 右值引用是右值吗? move语义与编译器优化 完美转发:问题

(译)C++11中的Move语义和右值引用

郑重声明:本文是笔者网上翻译原文,部分有做添加说明,所有权归原文作者! 地址:http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html C++一直致力于生成快速的程序.不幸的是,直到C++11之前,这里一直有一个降低C++程序速度的顽症:临时变量的创建.有时这些临时变量可以被编译器优化(例如返回值优化),但是这并不总是可行的,通常这会导致高昂的对象复制成本.我说的是怎么回事呢? 让我们

右值引用之移动语义

本文翻译自关于右值引用解释的经典文章,如果英文还可以的话,直接去看英文原文.thbecker.net/articles/rvalue_references/section_01.html 右值引用是c++中的一个特性,并且已经入驻c++11标准,可能大家一开始接触的时候感觉有点难以理解,但是他的确是很好用的一个玩意儿. 右值引用解决了两个问题: 1.move语义 2.完美转发. 我们接下来先简单介绍一下move语义,但是介绍move语义之前,我们需要先介绍左值和右值的含义. 在C语言中,我们给出

从4行代码看右值引用

从4行代码看右值引用 概述 右值引用的概念有些读者可能会感到陌生,其实他和C++98/03中的左值引用有些类似,例如,c++98/03中的左值引用是这样的: int i = 0; int& j = i; 这里的int&是对左值进行绑定(但是int&却不能绑定右值),相应的,对右值进行绑定的引用就是右值引用,他的语法是这样的A&&,通过双引号来表示绑定类型为A的右值.通过&&我们就可以很方便的绑定右值了,比如我们可以这样绑定一个右值: int&

C++ 右值引用:移动语义与完美转发

转载至: http://www.dutor.net/index.php/2013/11/rvalue-reference-move-semantics-and-perfect-forwarding/ C++11 引入的新特性中,除了并发内存模型和相关设施,这些高帅富之外,最引人入胜且接地气的特性就要属『右值引用』了(rvalue reference).加入右值引用的动机在于效率:减少不必要的资源拷贝.考虑下面的程序: 1 2 std::vector<string> v; v.push_back

Effective Modern C++:05右值引用、移动语义和完美转发

移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引用是将这两个不相关的语言特性连接起来的底层语言机制,正是它使得移动语义和完美转发成了可能. 23:理解std::move和std::forward std::move并不进行任何移动,std::forward也不进行任何转发.这两者在运行期都无所作为,它们不会生成任何可执行代码.实际上,std::m

[转][c++11]我理解的右值引用、移动语义和完美转发

c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象.所有的具名变量或者对象都是左值,而右值不具名.很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值. 看见书上又将右值分为将亡值和纯右值.纯右值就是c++98标准中右值的概念,

[c++11]右值引用、移动语义和完美转发

c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象.所有的具名变量或者对象都是左值,而右值不具名.很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值. 看见书上又将右值分为:将亡值和纯右值. 纯右值就是c++98标准中右值的概