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

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的赋值操作符函数,发生了以下事情:

首先要销毁name的字符串吧

把foo()返回的临时字符串拷贝到name吧

最后还要销毁foo()返回的临时字符串吧

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

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

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

templatevoid 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 typename remove_reference::type&& move(T&& t) {    using RRefType = typename remove_reference::type&&;    return static_cast(t);}

完美转发

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

templatevoid 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,代码看起来如下:

templatevoid fwd(T&& t){    foo(std::forward(t));}

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

templateT&& forward(typename remove_reference::type& t){    return static_cast(t);}

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

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

forward实例化:

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

引用折叠后:

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

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

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

forward实例化如下:

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

也就是:

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用和move语义是C++ 11中重要的特性之一,可以提高程序的效率和性能。引用是一种新的引用类型,其绑定到临时对象或将要销毁的对象上,而不是左对象。move语义则是利用引用,将一个对象的资源所有权从一个对象转移到另一个对象,避免了不必要的内存拷贝,提高了程序的效率。 下面是一个使用引用和move语义的例子: ```c++ #include <iostream> #include <vector> using namespace std; vector<int> getVector() { vector<int> v = {1, 2, 3, 4}; return v; } int main() { vector<int> v1 = getVector(); // 拷贝构造函数 vector<int> v2 = move(v1); // 移动构造函数 cout << "v1 size: " << v1.size() << endl; // 输出 0 cout << "v2 size: " << v2.size() << endl; // 输出 4 return 0; } ``` 在上面的例子中,getVector函数返回一个临时对象vector<int>,该临时对象是一个。在主函数中,我们使用拷贝构造函数将临时对象的拷贝到v1中,然后使用move函数将v1中的移动到v2中。由于move函数使用了引用,将v1中的资源所有权转移到了v2中,避免了不必要的内存拷贝,提高了程序的效率。最后,我们输出v1和v2的大小,可以看到v1的大小为0,v2的大小为4,说明资源已经成功转移。 需要注意的是,使用move语义之后,原对象的会被移动到新对象中,并且原对象的会被置为默认(例如,对于vector而言,原对象的大小为0)。如果需要保留原对象的,则需要在移动之前先进行一次拷贝操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值