C++技能系列 ( 7 ) - 右值引用、移动语意、完美转发

- 日理万妓 2024-03-16 21:19 79阅读 0赞

在这里插入图片描述

现在的一切都是为将来的梦想编织翅膀,让梦想在现实中展翅高飞。
Now everything is for the future of dream weaving wings, let the dream fly in reality.

右值引用、移动语意、完美转发

  • 1、右值引用
  • 2、完美转发

1、右值引用

右值引用(rvalue reference)是 C++11 为了实现移动语意(move semantic)和完美转发(perfect forwarding)而提出来的。
右值引用,简单说就是绑定在右值上的引用。右值的内容可以直接移动(move)给左值对象,而不需要进行开销较大的深拷贝(deep copy)。
移动语义
下面这个例子:
v2 = v1 调用的是拷贝赋值操作符,v2 复制了 v1 的内容 —— 复制语义。
v3 = std::move(v1) 调用的是移动赋值操作符,将 v1 的内容移动给 v3 —— 移动语义。

  1. std::vector<int> v1{
  2. 1, 2, 3, 4, 5};
  3. std::vector<int> v2;
  4. std::vector<int> v3;
  5. v2 = v1;
  6. std::cout << v1.size() << std::endl; // 输出 5
  7. std::cout << v2.size() << std::endl; // 输出 5
  8. v3 = std::move(v1); // move
  9. std::cout << v1.size() << std::endl; // 输出0
  10. std::cout << v3.size() << std::endl; // 输出 5

为了实现移动语意,C++ 增加了与拷贝构造函数(copy constructor)和拷贝赋值操作符(copy assignment operator)对应的移动构造函数(move constructor)和移动赋值操作符(move assignment operator),通过函数重载机制来确定应该调用拷贝语意还是移动语意(参数是左值引用就调用拷贝语意;参数是右值引用就调用移动语意)。 再来看一个简单的例子:

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. class Foo {
  5. public:
  6. // 默认构造函数
  7. Foo() {
  8. std::cout << "Default Constructor: " << Info() << std::endl; }
  9. // 自定义构造函数
  10. Foo(const std::string& s, const std::vector<int>& v) : s_(s), v_(v) {
  11. std::cout << "User-Defined Constructor: " << Info() << std::endl;
  12. }
  13. // 析构函数
  14. ~Foo() {
  15. std::cout << "Destructor: " << Info() << std::endl; }
  16. // 拷贝构造函数
  17. Foo(const Foo& f) : s_(f.s_), v_(f.v_) {
  18. std::cout << "Copy Constructor: " << Info() << std::endl;
  19. }
  20. // 拷贝赋值操作符
  21. Foo& operator=(const Foo& f) {
  22. s_ = f.s_;
  23. v_ = f.v_;
  24. std::cout << "Copy Assignment: " << Info() << std::endl;
  25. return *this;
  26. }
  27. // 移动构造函数
  28. Foo(Foo&& f) : s_(std::move(f.s_)), v_(std::move(f.v_)) {
  29. std::cout << "Move Constructor: " << Info() << std::endl;
  30. }
  31. // 移动赋值操作符
  32. Foo& operator=(Foo&& f) {
  33. s_ = std::move(f.s_);
  34. v_ = std::move(f.v_);
  35. std::cout << "Move Assignment: " << Info() << std::endl;
  36. return *this;
  37. }
  38. std::string Info() {
  39. return "{" + (s_.empty() ? "'empty'" : s_) + ", " +
  40. std::to_string(v_.size()) + "}";
  41. }
  42. private:
  43. std::string s_;
  44. std::vector<int> v_;
  45. };
  46. int main() {
  47. std::vector<int> v(1024);
  48. std::cout << "================ Copy =======================" << std::endl;
  49. Foo cf1("hello", v);
  50. Foo cf2(cf1); // 调用拷贝构造函数
  51. Foo cf3;
  52. cf3 = cf2; // 调用拷贝赋值操作符
  53. std::cout << "================ Move =========================" << std::endl;
  54. Foo f1("hello", v);
  55. Foo f2(std::move(f1)); // 调用移动构造函数
  56. Foo f3;
  57. f3 = std::move(f2); // 调用移动赋值操作符
  58. return 0;
  59. }

简单封装了一个类 Foo,重点是实现:
拷贝语意:拷贝构造函数 Foo(const Foo&) 、拷贝赋值操作符 Foo& operator=(const Foo&) 。
移动语意:移动构造函数 Foo(Foo&&) 、移动赋值操作符 Foo& operator=(Foo&&) 。
拷贝语意相信大部分人都比较熟悉了,也比较好理解。在这个例子中,每次都会拷贝 s_ 和 v_ 两个成员,最后 cf1、cf2、cf3 三个对象的内容都是一样的。 每次执行移动语意,是分别调用 s_ 和 v_ 的移动语意函数——理论上只需要对内部指针进行修改,所以效率较高。执行移动语意的代码片段了出现了一个标准库中的函数 std::move —— 它可以将参数强制转换成一个右值。本质上是告诉编译器,我想要 move 这个参数——最终能不能 move 是另一回事——可能对应的类型没有实现移动语意,可能参数是 const 的。 有一些场景可能拿到的值直接就是右值,不需要通过 std::move 强制转换,比如:

  1. Foo GetFoo() {
  2. return Foo("GetFoo", std::vector<int>(11));
  3. }
  4. ....
  5. Foo f3("world", v3);
  6. ....
  7. f3 = GetFoo(); // GetFoo 返回的是一个右值,调用移动赋值操作符

2、完美转发

C++ 通过了一个叫 std::forward 的函数模板来实现完美转发。这里直接使用 Effective Modern C++ 中的例子作为说明。在前面的例子上,我们增加如下的代码:

  1. // 接受一个 const 左值引用
  2. void Process(const Foo& f) {
  3. std::cout << "lvalue reference" << std::endl;
  4. // ...
  5. }
  6. // 接受一个右值引用
  7. void Process(Foo&& f) {
  8. std::cout << "rvalue reference" << std::endl;
  9. // ...
  10. }
  11. template <typename T>
  12. void LogAndProcessNotForward(T&& a) {
  13. std::cout << a.Info() << std::endl;
  14. Process(a);
  15. }
  16. template <typename T>
  17. void LogAndProcessWithForward(T&& a) {
  18. std::cout << a.Info() << std::endl;
  19. Process(std::forward<T>(a));
  20. }
  21. LogAndProcessNotForward(f3); // 输出 lvalue reference
  22. LogAndProcessNotForward(std::move(f3)); // 输出 lvalue reference
  23. LogAndProcessWithForward(f3); // 输出 lvalue reference
  24. LogAndProcessWithForward(std::move(f3)); // 输出 rvalue reference

LogAndProcessNotForward(f3); 和 LogAndProcessWithForward(f3); 都输出 “lvalue reference”,这一点都不意外,因为 f3 本来就是一个左值。
LogAndProcessNotForward(std::move(f3)); 输出 “lvalue reference” 是因为虽然参数 a 绑定到一个右值,但是参数 a 本身是一个左值。
LogAndProcessWithForward(std::move(f3)); 使用了 std::forward 对参数进行转发,std::forward 的作用就是:当参数是绑定到一个右值时,就将参数转换成一个右值。
左值?右值?
到底什么时候是左值?什么时候是右值?是不是有点混乱? 在 C++ 中,每个表达式(expression)都有两个特性:
has identity? —— 是否有唯一标识,比如地址、指针。有唯一标识的表达式在 C++ 中被称为 glvalue(generalized lvalue)。
can be moved from? —— 是否可以安全地移动(编译器)。可以安全地移动的表达式在 C++ 中被成为 rvalue。
根据这两个特性,可以将表达式分成 4 类:
has identity and cannot be moved from - 这类表达式在 C++ 中被称为 lvalue。
has identity and can be moved from - 这类表达式在 C++ 中被成为 xvalue(expiring value)。
does not have identity and can be moved from - 这类表达式在 C++ 中被成为 prvalue(pure rvalue)。
does not have identity and cannot be moved -C++ 中不存在这类表达式。
简单总结一下这些 value categories 之间的关系:
可以移动的值都叫 rvalue,包括 xvalue 和 prvalue。
有唯一标识的值都叫 glvalue,包括 lvalue 和 xvalue。
std::move 的作用就是将一个 lvalue 转换成 xvalue。

发表评论

表情:
评论列表 (有 0 条评论,79人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C++ 引用

    右值引用 -------------------- 今天平安夜,要不是女巫用药,要不是狼人一刀刀长老身上了。。。 开个玩笑。。。 右值引用