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 = 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 = 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 = 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));
}

参考资料

https://zhuanlan.zhihu.com/p/...

你可能感兴趣的