右值引用、移动语义

一、 什么是左值、右值

首先不考虑引用以减少干扰,可以从2个角度判断:

  • 左值可以取地址、位于等号左边
  • 而右值没法取地址,位于等号右边

 

比如:

int num = 3;
  • num可以通过 & 取地址,位于等号左边,所以num是左值。
  • 3位于等号右边,3没法通过 & 取地址,所以3是个右值。
再者:

class Test {
public:
    Test(int num = 0) {
        _num = num;
    }
private:
    int _num;
};

void main(){
	Test test=Test();
}
  •  同样的,test可以通过 & 取地址,位于等号左边,所以test是左值。
  • Test()是个临时值,没法通过 & 取地址,位于等号右边,所以Test()是个右值。

因此,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

 

 

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

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

1、左值引用

     顾名思义,左值引用=对左值的引用。能指向左值,不能指向右值的

int num = 3;
int &ref_num = num; // 左值引用指向左值,编译通过
int &ref_num = 3; // 左值引用指向了右值,会编译失败

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

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

const int &ref_num = 3;  // 编译通过

const左值引用不会修改指向值,因此可以指向右值。

正是这个原因,函数参数的用法:void Test(const std::string& value);void Test(const std::vector<std::string>& vet)等等。

 

 

2、右值引用

同理,右值引用=对右值的引用。能指向右值,不能指向左值的。

int &&right_num = 3; // ok
 
int num = 3;
int &&left_num = num; // 编译不过,右值引用不可以指向左值
 
right_num = 4; // 右值引用的用途:可以修改右值

 

三、右值引用能否指向左值

    在C++11中,引入了新特性:std::move

int num = 3; // num是个左值
int &left_num = num; // 左值引用指向左值
int &&right_num = std::move(num); // 通过std::move将左值转化为右值,可以被右值引用指向
 
cout << num; // 打印结果:3

本质:

  • 其实std::move语义其本质并非将一个值移动到另一个值,释放原内存。
  • 而是把左值强制转化为右值,让右值引用可以指向左值。
  • 其实现等同于一个类型转换:static_cast<T&&>(lvalue)

同理,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_num = 3;
ref_num = 4; 

等同于以下代码:
int temp = 30;
int &&ref_num = std::move(temp);
ref_num = 40;

 

四、左值引用、右值引用本身是左值还是右值?

     被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。

// 形参是个右值引用
void ChangeValue(int&& right_value) { 
     right_value = 8;
}

int main() {
int num = 3; // a是个左值
int &ref_a_left = num // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用

ChangeValue(num); // 编译不过,num是左值,change参数要求右值
ChangeValue(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
ChangeValue(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值

ChangeValue(std::move(a)); // 编译通过
ChangeValue(std::move(ref_a_right)); // 编译通过
ChangeValue(std::move(ref_a_left)); // 编译通过

change(5); // 当然可以直接接右值,编译通过

// 打印这三个左值的地址,都是一样的
cout << &num << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
}

 

最后,std::move会返回一个右值引用int &&,它是左值还是右值呢?

        从表达式int &&ref = std::move(num)来看,右值引用ref指向的必须是右值,所以move返回的int &&是个右值。所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值

 

结论:

1、从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。

2、右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。

3、作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

 

 

代码上的实践

// std::vector和std::string的实际例子
int main() {
    std::string str1 = "aacasxs";
    std::vector<std::string> vec;
     
    vec.push_back(str1); // 传统方法,copy
    vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
    vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
    vec.emplace_back("axcsddcas"); // 当然可以直接接右值
}

在vector和string这个场景,加个std::move会调用到移动语义函数,避免了深拷贝。

可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

比如:

moveable_objecta = moveable_objectb; 
改为: 
moveable_objecta = std::move(moveable_objectb);

还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):

std::unique_ptr<Test> ptr_test1 = std::make_unique<Test>();

// unique_ptr只有‘移动赋值重载函数‘,参数是&& ,只能接右值,因此必须用std::move转换类型
std::unique_ptr<Test> ptr_test2 = std::move(ptr_test1); // 编译不通过
std::unique_ptr<Test> ptr_test2 = ptr_test1; 

std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。

 

 

此外,还有完美转发 std::forward,不过forward多用于模板,后续再补吧。。。

 

posted @ 2021-07-18 21:11  gd_沐辰  阅读(236)  评论(0编辑  收藏  举报