C++右值引用与移动语义

背景 开始学习C++11和准备秋招面试时,对右值引用和移动语义进行的深入学习,恰巧在面试中又被问到,深入记录一下。
左值/右值 左值:可以取地址、位于等号左边 -> 有地址的变量
右值:没法取地址、位于等号右边 -> 没有地址的字面值、临时值
两个例子:

int a = 5;

  • a->可以通过 & 取地址,位于等号左边,是左值。
  • 5位于等号右边,5没法通过 & 取地址,所以5是个右值。
struct A { A(int a = 0) { a_ = a; } int a_; }; A a = A();

  • a ------------>可以通过&取地址,位于等号左边,是左值
  • A()-> 临时值,没法通过&取地址,位于等号右边,是右值
    左值引用/右值引用引用的本质是别名。
    通过引用修改变量的值,传参时传引用可以避免拷贝。
    左值引用左值引用:能指向左值,不能指向右值的引用
    引用时变量的别名,右值没有地址无法被修改
    const左值引用可以指向右值(不会修改指向值,可以指向右值)
    int a = 5; int &ref_left_a = a; //左值引用指向左值,编译通过 int &ref_left_a = 5; //左值引用指向右值,编译失败 const int &ref_left_a = 5; //编译通过

    右值引用右值引用:可以指向右值,不能指向左值
    int a = 5; int &&ref_right = 5; //编译通过 int &&ref_a_right = a; //编译不通过ref_right = 6; //右值引用:可以修改右值

    左/右值引用本质的讨论右值指向左值的方法
    使用std::move
    int a = 5; int &ref_a_left = a; // 左值引用指向左值 int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向cout << a; // 打印结果:5

    std::move唯一的功能:把左值强制转换为右值,让右值引用可以指向左值
    等同实现:static_cast(lvalue);
    左/右值引用本身是什么?
    被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
    void test(int&& right_value) { right_value = https://www.it610.com/article/8; } int main() { int a = 5; // a是个左值 int &ref_a_left = a; // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用test(a); // 编译不过,a是左值,change参数要求右值 test(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 test(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值test(std::move(a)); // 编译通过 test(std::move(ref_a_right)); // 编译通过 test(std::move(ref_a_left)); // 编译通过 test(5); // 当然可以直接接右值,编译通过cout << &a <<' '; cout << &ref_a_left << ' '; cout << &ref_a_right; // 打印这三个左值的地址,都是一样的 }

    右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。
    作为函数返回值的 && 是右值,直接声明出来的 && 是左值。
相同点:
传参使用左右值引用都可以避免拷贝。
不同点:
右值引用可以直接指向右值,也可以通过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); }

右值引用和std::move的应用 右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。
实现移动语义 以数组类举例:
class Array { public: Array(int size) : size_(size) { data = https://www.it610.com/article/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_; } private: int *data_; int size_; };

提供一个移动构造函数,把被拷贝者的数据移动过来,这样就可以避免深拷贝了
class Array { public: Array(int size) : size_(size) { data = https://www.it610.com/article/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_; }private: int *data_; int size_; };

存在的两个问题:
1、表示移动语义还需要一个额外的参数(或者其他方式)
2、无法实现!temp_array是个const左值引用,无法被修改
右值引用出现解决问题:
class Array { public: ...... // 优雅 Array(Array&& temp_array) { data_ = temp_array.data_; size_ = temp_array.size_; // 为防止temp_array析构时delete data,提前置空其data_ temp_array.data_ = nullptr; }private: int *data_; int size_; };

其他:
1、vector::push_back使用std::move提高性能
2、部分是move-only,例如unique_ptr,只有移动构造函数
完成转发 std::forward std::forward并不会做转发,同样也是做类型转换。
move只能转出来右值,forward都可以。
std::forward(u)有两个参数:T与 u。
1、当T为左值引用类型时,u将被转换为T类型的左值;
2、否则u将被转换为T类型右值。
void B(int&& ref_r) { ref_r = 1; } // A、B的入参是右值引用 // 有名字的右值引用是左值,因此ref_r是左值 void A(int&& ref_r) { B(ref_r); // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过 B(std::forward(ref_r)); // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值 } int main() { int a = 5; A(std::move(a)); }

参考资料 【C++右值引用与移动语义】https://zhuanlan.zhihu.com/p/...

    推荐阅读