cpp_1_3_右值引用 Rvalue reference

  1. Overview
  2. Rvalue Reference 右值引用
  3. Move constructor and Move assignment 移动构造和移动赋值
  4. std::move 和 std::forward
  5. Reference

Overview

1.Rvalue Reference 右值引用
2.Move constructor and Move assignment 移动构造和移动赋值
3.std::move 和 std::forward

Rvalue Reference 右值引用

1.c++11 新增了一种引用, 叫做 rvalue reference (右值引用), 右值引用只能用右值初始化. 左值引用使用一个&符号创建, 而右值引用采用 && 符号创建

int x = 5;
int& lref = x;
int&& rref = 5;

2.右值引用只能用右值初始化, 不能被左值初始化
3.右值引用将初始化它们的对象的生命周期延长到右值引用的生命周期 (对 const 对象的左值引用也可以做到这一点)
4.右值引用允许修改右值
5.函数几乎永远不会有返回右值引用/左值引用的情形

#include <iostream>

int main() {
  int&& r_ref = 5;   // 使用字面量初始化右值引用, 引用一个临时的对象
  r_ref = 10;        // 右值引用可以修改右值
  std::cout << r_ref << std::endl;

  int a = 5;
  int&& r_ref_a = a;  // 编译不通过, 右值引用不可以指向右值
                    // "rvalue reference to type 'int' cannot bind to lvalue of type 'int'",

  const int&& r_ref8 = 8;
  std::cout << r_ref8 << std::endl;
  return 0;
}

6.右值引用有办法指向左值吗?
(1). 采用==std::move可以讲左值强制转化为右值==, 让右值引用可以指向左值, 其实现等同于一个类型转换

static_cast<T&&>(lvalue);

采用std::move我们将右值转为左值, 例如

#include <iostream>

int main() {
  int &&r_ref_a = 5;
  r_ref_a = 6; 

  // 等同于以下代码:
  int temp = 5;
  int &&r_ref_b = std::move(temp);
  r_ref_b = 6;
  std::cout << r_ref_b << std::endl;
  return 0;
}

(2). 单纯的std::move(xx) 不会有性能的提升

7.右值引用通常不会像如上的方式这么直接定义, ==右值引用通常用作函数的参数==; 从性能上讲左值引用和右值引用没有区别, 左值右值都能避免copy操作; 右值引用可以指向右值或者通过std::move指向左值; 而普通的左值引用只能指向左值, const左值引用也能指向右值
8.这里有个需要注意的点是, 左值引用和右值引用本身是左值还是右值? ==左值引用和右值引用本身都是左值==. 注意分析以下的代码:

#include <iostream>
using namespace std;

void passConstLvalue(const int& const_value) {
  cout << const_value;
}

void passRvalue(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 是个右值引用

  passConstLvalue(a);               // 编译通过
  passConstLvalue(ref_a_left);      // 编译通过
  passConstLvalue(ref_a_right);     // 编译通过

  passRvalue(a);                    // 编译不过,a 是左值,change参数要求右值
  passRvalue(ref_a_left);           // 编译不过,注意: 左值引用 ref_a_left 本身是个左值
  passRvalue(ref_a_right);          // 编译不过,注意: 右值引用 ref_a_right 本身是个左值

  passRvalue(std::move(a));           // 编译通过
  passRvalue(std::move(ref_a_right)); // 编译通过
  passRvalue(std::move(ref_a_left));  // 编译通过
  passRvalue(5);                      // 当然可以直接接右值,编译通过

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

8.当希望左值引用参数和右值引用参数具有不同的行为, 也就是发生重载的时候, 可以用左值引用也可以右值, 虽然const左值引用也可以接受, 但是传const左值引用无法修改值, 存在一定的局限性

#include <iostream>

void fun(const int& lref) {
  std::cout << "l-value reference to const: " << lref << '\n';
}

void fun(int&& rref) {
  std::cout << "r-value reference: " << rref << '\n';
}

int main() {
  int x = 5;
  fun(x); // 传递左值, 找对应的左值引用形参版本
  fun(5); // 传递右值, 找对应的右值引用形参版本
  return 0;
}

Move constructor and Move assignment 移动构造和移动赋值

1.重温定义: copy constructor通过复制同类的对象来初始化类, copy assignment用于将一个类的对象复制到另一个现有的对象, 如果没有显示提供cpp会默认提供, 这些默认提供的函数会执行shallow copy.
2.shallow copy这会给分配动态内存的类带来问题, 因此处理动态内存的类应该重写这些function实现deep copy. 以下是实现copy constructor和copy assignment采用deep copy的一种实现

#include <iostream>
using namespace std;

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_;
};

int main() {
  return 0;
}

3.c++11 定义了两个为 move senamics (移动语义) 服务的函数, move constructor 和 move assignment, copy consturctor 和 copy assignment 的目标是将资源的所有权从一个对象转移到另一个对象, 而==move constructor 和 move assigment 的目标是将资源的所有权从一个转移到另一个对象==, 通常节省更多的资源
4.定义 move constructor 和 move assignment 通常和 copy consturctor 和 copy assignment 定义非常相似, 但是函数参数输入从 const lvalue reference 改变成为 non-const rvalue reference, 也就说用右值引用作为函数参数
5.移动语义的 Key insight:
(1). 如果我们要在参数是左值的情况下, 想构造一个对象或赋值, 那么我们唯一可以合理做的就是复制左值. 我们不能假设更改左值是安全的, 因为它可能会在程序稍后再次使用. 如果我们有一个表达式”a = b” (其中 b 是左值, 我们不能期望b不改变.
(2). 如果我们要在参数是右值的情况下, 想构造一个对象或赋值, 那么我们就知道右值只是某种临时对象. 我们可以简单地将其资源(很低成本地)转移到我们正在构造或分配的对象, 而不是复制它 (copy成本很高). 这样做是安全的, 因为临时变量无论如何都会在表达式末尾被销毁, 所以我们知道它永远不会再被使用
(3). c++ 通过右值引用, 使我们能够在参数是左值和右值两种情况下提供不同的行为, 使我们能够就对象的行为方式做出更明智且更高效的决策

#include <iostream>
using namespace std;

class Array {
 public:
  Array(int size) : size_(size) {
    data_ = new int[size_];
  }

  // 实现右值引用
  Array(Array&& temp_array) {
    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_;
};

int main() {
  Array* a = new Array(5);

  Array* b(std::move(a));
  return 0;
}

std::move 和 std::forward

1.std::move可以将左值强制转化为右值, 但是右值参数不能接受左值输入, 比如存在一种情况右值参数输入右值引用(右值引用是左值)是不允许的, 所以这种情况用一次std:move转成右值; std::forward (完美转发) 可以实现更通用的左值和右值的互换
2.std::forward(u)接受两个参数, 当T为左值引用类型, std::forward将 u 转化为 T 类型的左值; std:forward当T为右值引用类型, 将 u 转化为 T 的右值引用类型

#include <iostream>
using namespace std;

void B(int&& ref_r) {
  ref_r = 1;
}

void A(int&& ref_r) {
  B(ref_r);                     // 编译不过B的入参是右值引用,需要接右值,ref_r是左值,编译失败

  B(std::move(ref_r));          // ok,std::move把左值转为右值,编译通过
  B(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}

int main() {
  int a = 5;
  A(std::move(a));
  return 0;
}

Reference

1.https://www.learncpp.com/cpp-tutorial/move-constructors-and-move-assignment/.
2.https://zhuanlan.zhihu.com/p/335994370. 一文读懂C++右值引用和std::move.


转载请注明来源, from goldandrabbit.github.io

💰

×

Help us with donation