Loading [Contrib]/a11y/accessibility-menu.js

C++右值引用

 


 

左值、右值

1 左值是表达式结束后依然存在的持久对象

2 右值是表达式结束后不再存在的临时对象

简单来说,能取地址的是左值,否则就是右值。

 

右值引用的意义

实现移动语义完美转发

移动语义:可以用廉价的移动操作来代替昂贵的拷贝操作。

完美转发:将实参转发到其他的函数,使目标函数接收到的实参与被传递给转发函数的实参保持一致。

 

移动语义

C++11的右值引用和std::move可以实现移动语义,通过减少拷贝操作提升效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class A{
 
public:
    A(): size(0), data(nullptr){}
    A(size_t size): size(size){
        data = new int[size];
    }
 
    A(const A& rhs){
        size = rhs.size;
        data = new int[size];
        for(int i=0; i<size; ++i){
            data[i] = rhs.data[i];
        }
        cout << "Copy constructor" << endl;
    }
 
    A& operator=(const A& rhs){
 
        if(this == &rhs){
            return *this;
        }
 
        delete[] data;
 
        size = rhs.size;
        data = new int[size];
        for(int i=0; i<size; ++i){
            data[i] = rhs.data[i];
        }
        cout << "Copy assignment" << endl;
        return *this;
    }
 
 
    ~A(){
        delete[] data;
    }
 
private:
    int *data;
    size_t size;
};

对于上面的类来说,无论是拷贝构造还是拷贝赋值,都需要将源数据一一拷贝,但是如果源数据在拷贝之后就不需要了,我们直接把源数据拿过来不是省事多了吗?对此我们只需要修改一下指针地址即可,在代码中加入下面的移动构造函数。

1
2
3
4
5
6
7
A(A&& rhs){
    data = rhs.data;
    size = rhs.size;
    rhs.size = 0;
    rhs.data = nullptr;
    cout << "Move constructor" << endl;
}

于是通过std::move将源数据转化为右值后就会调用相应的移动构造函数。

1
2
3
4
int main(){
    A a1 = A(10);
    A a2(std::move(a1));
}

 

完美转发

首先看第一个例子:

1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
 
int main(){
    int x = 5;
    int& left = x;
    int&& right = std::move(x);
    //cout << &left << endl << &right << endl;
}

在上面的代码中,被声明的左值引用和右值引用都是左值,是可以输出它们的地址的。

 

然后看二个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
 
void func(int& x){
    cout << "左值" <<endl;
}
 
void func(int&& x){
    cout << "右值" <<endl;
}
 
void test(int&& x){
    func(x);
}
 
int main(){
    test(5);
}

在上面的代码中,最后的输出是左值,虽然传给 test 函数的是一个右值,然而在调用 func 时,使用的是 x,参数有了名称,联系第一个例子,此时 x 是一个左值。

如果想要最后的输出是右值,需要使用 std::forward,而 std::forward 中涉及到了引用折叠。

 

引用折叠

对于T&、T&&来说,其引用类型未定,可能是左值引用,也可能是右值引用,需要视传入的实参而定。

T& &  T& 左值引用的左值引用折叠为左值引用
T& && T& 右值引用的左值引用折叠为左值引用
T&& & T& 左值引用的右值引用折叠为左值引用
T&& && T&& 右值引用的右值引用折叠为右值引用

简而言之,只有两者均为右值引用时,最后的引用类型才为右值引用,否则均为左值引用

 

源码分析

 remove_reference 源码

1
2
3
4
5
6
7
8
9
10
11
12
template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };
  
// 特化版本
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };
  
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };

std::forward 源码

1
2
3
4
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

根据上述源码,首先通过remove_reference获取 _Tp 的类型 type,然后声明左值引用变量 __t 。

根据 _Tp 的不同,_Tp 会发生引用折叠:

  1. 当 _Tp 为左值引用时,_Tp折叠为左值引用
  2. 当 _Tp 为右值引用时,_Tp折叠为右值引用

可以发现当 std::forward 的输入是左值引用时,输出也是左值引用;输入是右值引用时,输出也是右值引用。

 

在下面的代码中,使用了 std::forward 之后就会输出右值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
 
void func(int& x){
    cout << "左值" <<endl;
}
 
void func(int&& x){
    cout << "右值" <<endl;
}
 
void test(int&& x){
    func(std::forward<int>(x));
}
 
int main(){
    test(5);
}

  

 

std::move 和 std::forward 的区别

  1. std::move 不移动(move)任何东西,也不保证它执行转换的对象可以被移动,它只执行到右值的无条件的转换
  2. std::forward也不转发(forward)任何东西,只有当它的参数被绑定到一个右值时,才将参数转换为右值。
  3. 一个是无条件的,另一个是有条件的,所以有不同的应用场景。

 

 

 

References:

  1. 一文带你详细介绍c++中的std::move函数
  2. C++ 理解std::forward完美转发
  3. 一文读懂C++右值引用和std::move
  4. 第5章 右值引用,移动语义,完美转发


如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
posted @   Kayden_Cheung  阅读(110)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
//目录
点击右上角即可分享
微信分享提示