现代 C++:右值引用、移动语意、完美转发

右值引用(rvalue reference)是 C++11 为了实现移动语意(move semantic)和完美转发(perfect forwarding)而提出来的。

右值引用,简单说就是绑定在右值上的引用。右值的内容可以直接移动(move)给左值对象,而不需要进行开销较大的深拷贝(deep copy)。

移动语义

下面这个例子:

  1. v2 = v1 调用的是拷贝赋值操作符,v2 复制了 v1 的内容 —— 复制语义。
  2. v3 = std::move(v1) 调用的是移动赋值操作符,将 v1 的内容移动给 v3 —— 移动语义。
  std::vector<int> v1{1, 2, 3, 4, 5}; 
  std::vector<int> v2; 
  std::vector<int> v3; 

  v2 = v1; 
  std::cout << v1.size() << std::endl;  // 输出 5
  std::cout << v2.size() << std::endl;  // 输出 5

  v3 = std::move(v1); // move
  std::cout << v1.size() << std::endl;  // 输出0
  std::cout << v3.size() << std::endl;  // 输出 5

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

#include <iostream>
#include <string>
#include <vector>

class Foo {
 public:
  // 默认构造函数
  Foo() { std::cout << "Default Constructor: " << Info() << std::endl; }

  // 自定义构造函数
  Foo(const std::string& s, const std::vector<int>& v) : s_(s), v_(v) {
    std::cout << "User-Defined Constructor: " << Info() << std::endl;
  }

  // 析构函数
  ~Foo() { std::cout << "Destructor: " << Info() << std::endl; }

  // 拷贝构造函数
  Foo(const Foo& f) : s_(f.s_), v_(f.v_) {
    std::cout << "Copy Constructor: " << Info() << std::endl;
  }

  // 拷贝赋值操作符
  Foo& operator=(const Foo& f) {
    s_ = f.s_;
    v_ = f.v_;
    std::cout << "Copy Assignment: " << Info() << std::endl;
    return *this;
  }

  // 移动构造函数
  Foo(Foo&& f) : s_(std::move(f.s_)), v_(std::move(f.v_)) {
    std::cout << "Move Constructor: " << Info() << std::endl;
  }

  // 移动赋值操作符
  Foo& operator=(Foo&& f) {
    s_ = std::move(f.s_);
    v_ = std::move(f.v_);
    std::cout << "Move Assignment: " << Info() << std::endl;
    return *this;
  }

  std::string Info() {
    return "{" + (s_.empty() ? "'empty'" : s_) + ", " +
           std::to_string(v_.size()) + "}";
  }

 private:
  std::string s_;
  std::vector<int> v_;
};

int main() {
  std::vector<int> v(1024);

  std::cout << "================ Copy =======================" << std::endl;
  Foo cf1("hello", v);
  Foo cf2(cf1);  // 调用拷贝构造函数
  Foo cf3;
  cf3 = cf2;  // 调用拷贝赋值操作符
  
  std::cout << "================ Move =========================" << std::endl;
  Foo f1("hello", v);
  Foo f2(std::move(f1));  // 调用移动构造函数
  Foo f3;
  f3 = std::move(f2);  // 调用移动赋值操作符
  return 0;
}

简单封装了一个类 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 强制转换,比如:

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

完美转发

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

// 接受一个 const 左值引用
void Process(const Foo& f) {
  std::cout << "lvalue reference" << std::endl;
  // ...
}

// 接受一个右值引用
void Process(Foo&& f) {
  std::cout << "rvalue reference" << std::endl;
  // ...
}

template <typename T>
void LogAndProcessNotForward(T&& a) {
  std::cout << a.Info() << std::endl;
  Process(a); 
}

template <typename T>
void LogAndProcessWithForward(T&& a) {
  std::cout << a.Info() << std::endl;
  Process(std::forward<T>(a));
}

 LogAndProcessNotForward(f3);                         // 输出 lvalue reference
 LogAndProcessNotForward(std::move(f3));  // 输出 lvalue reference

 LogAndProcessWithForward(f3);                        // 输出 lvalue reference
 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)都有两个特性:

  1. has identity? —— 是否有唯一标识,比如地址、指针。有唯一标识的表达式在 C++ 中被称为 glvalue(generalized lvalue)。
  2. can be moved from? —— 是否可以安全地移动(编译器)。可以安全地移动的表达式在 C++ 中被成为 rvalue。

根据这两个特性,可以将表达式分成 4 类:

  1. has identity and cannot be moved from - 这类表达式在 C++ 中被称为 lvalue。
  2. has identity and can be moved from - 这类表达式在 C++ 中被成为 xvalue(expiring value)。
  3. does not have identity and can be moved from - 这类表达式在 C++ 中被成为 prvalue(pure rvalue)。
  4. does not have identity and cannot be moved -C++ 中不存在这类表达式。

简单总结一下这些 value categories 之间的关系:

  1. 可以移动的值都叫 rvalue,包括 xvalue 和 prvalue。
  2. 有唯一标识的值都叫 glvalue,包括 lvalue 和 xvalue。
  3. std::move 的作用就是将一个 lvalue 转换成 xvalue。

这些概念其实有点绕。不过就算不是特别清楚这些概念,也不影响我们对移动语义的利用。

参考文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342