右值引用详解 右值引用解决了什么问题

1 前言
右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vector、std::string 之类的额外开销。

先说一下一个看到的结论,引入右值引用,就是为了移动语义。移动语义就是为了减少拷贝。std::move就是将左值转为右值引用。这样就可以重载到移动构造函数了,移动构造函数将指针赋值一下就好了,不用深拷贝了,提高性能

2 什么是左值、右值、纯右值、将亡值
左值,lvalue,就是赋值符号左边的值,如a=5,a就是左值,但是准确的来说,左值是表达式(不一定是赋值表达式)后仍然存在的持久对象。

右值,rvalue,右边的值,是指表达式结束就不存在的临时对象。

纯右值,prvalue,用于计算的或者用于初始化对象的的右值。

( 在介绍将亡值之前对左右和右值做个总结,有地址的变量就是左值,没有地址的字面值、临时值就是右值。)

将亡值,xvalue,是C++11为了引入右值引用而提出的概念,与纯右值的不同点在于,将亡值是即将被销毁、却能够被移动的值。可能比较难理解,如下代码:
 

std::vector<int> foo(){
    std::vector<int> temp = {1,2,3,4,5};
    return temp;
}
std::vector<int> v = foo();

上段代码,函数foo的返回值temp在内部创建然后赋值给v,然而v或者这个对象时会将整个temp拷贝一份,然后把temp销毁,如果这个temp非常大,将会造成大量额外的开销。最后一行中v是左值,foo()返回的值是纯右值。但是v可以被别的变量捕获到,而foo()产生的那个返回值作为一个临时值,一旦被v复制后,将立即被销毁也不能修改,将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。

在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 static_cast<std::vector<int> &&>(temp),进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。

3 什么是左值引用、右值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝,其实现原理和指针类似。

3.1 左值引用

只能指向左值,不能指向右值的就是左值引用
 

只能指向左值,不能指向右值的就是左值引用

int a = 5;
int &t = a; //左值引用指向左值,编译通过
int &t = 5; //左值引用指向右值,编译失败
引用是对变量起别名,由于右值没有地址无法被修改,所以左值引用无法指向右值。

但是 const 左值引用 可以指向右值的:

const int &t = 5 //编译通过
为什么?因为const 左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const &作为函数参数的原因之一,如std::vector的push_back:

void push_back (const value_type& val);
如果没有const,vec.push_back(5)这样的代码就无法编译通过了。

3.2 右值引用
右值引用和左值引用的区别就是,右值引用只能指向右值,不能指向左值,符号是&&,比左值引用多了一个&。

int &&t = 5;//编译通过
int a = 5;
int &&t = a;//编译失败
 
t = 6; //右值引用的用于:可以修改右值
左值引用默认只能指向左值,但是加了const的情况下可以指向右值,那么右值引用有没有类似的机制来指向左值呢?有的,就是std::move。

int a = 5;
int &l = a;  //正确,左值引用只能指向左值
//int &&r = 5; //正确,右值引用只能指向右值
//int &&r = a; //编译错误
int &&r = std::move(a); //正确,通过std::move将左值转化为右值,可以被右值引用指向左值。
cout<< a;  打印结果5
std::move 唯一的功能是把左值强制转换为右值,可以让右值引用指向左值,等于一个强制类型转换。

int &&ref_a = 5;
ref_a = 6; 
 
等同于以下代码:
 
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
注意,被声明出来的左、右值引用都是左值,因为被声明出的左右值引用是有地址的,也位于等号左边。即int &t,int &&t本身都是左值。但是,i&&t也可以是右值,作为函数返回值的 && 是右值,直接声明出来的 && 是左值。

总结:

从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性
void f(const int& n) {
    n += 1; // 编译失败,const左值引用不能修改指向变量
}
 
void f2(int && n) {
    n += 1; // ok
}
 
int main() {
    f(5);
    f2(5);
}
4 右值引用和std::move的应用场景
4.1 实现移动语义
在实际场景中,右值引用和std::move被广泛应用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数、拷贝构造函数、赋值运算符重载、析构函数等。

class Array {
public:
    Array(int size) : size_(size) {
        data = new int[size_];
    }
     
    // 深拷贝构造
    Array(const Array& temp_array) {
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
     
    // 深拷贝赋值
    Array& operator=(const Array& temp_array) {
        delete[] data_;
 
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
 
    ~Array() {
        delete[] data_;
    }
 
public:
    int *data_;
    int size_;
};
上面代码的深拷贝构造、深拷贝复制在传参数的时候已经通过左值引用避免了一次多余的拷贝了,但是内部还是需要实现深拷贝,即重新开辟空间、赋值,要需要解决这个问题, 这时,有人提出一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了,但是问题比较多,比如值:

class Array {
public:
    Array(int size) : size_(size) {
        data = new int[size_];
    }
     
    // 深拷贝构造
    Array(const Array& temp_array) {
        ...
    }
     
    // 深拷贝赋值
    Array& operator=(const Array& temp_array) {
        ...
    }
 
    // 移动构造函数,可以浅拷贝
    Array(const Array& temp_array, bool move) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
    ~Array() {
        delete [] data_;
    }
 
public:
    int *data_;
    int size_;
};
这么做有2个问题:

不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。当然函数参数可以改成非const:Array(Array& temp_array, bool move){...},这样也有问题,由于左值引用不能接右值,Array a = Array(Array(), true);这种调用方式就没法用了。
可以发现左值引用真是用的很不爽,右值引用的出现解决了这个问题,在STL的很多容器中,都实现了以右值引用为参数的移动构造函数和移动赋值重载函数,或者其他函数,最常见的如std::vector的push_back和emplace_back。参数为左值引用意味着拷贝,为右值引用意味着移动。
————————————————
版权声明:本文为CSDN博主「HR_Reborn」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/HR_Reborn/article/details/130363997

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值