【转载】C++11 朝码夕解: move 和 forward

230 阅读5分钟

原文地址:minminC++11 朝码夕解: move 和 forward

以下是正文部分,我对重要的部分进行了标注,并对排版和错别字进行了一点小修改**

move 和f orward 虽然是 8 年前 C++ 提出的新东西, 但要搞懂还是得费一些精力.

网上有挺多相关材料, 但即使是 stackoverflow , 对 move 和 forward 的讲解, 要么抠底层抠到入土, 要么东拉西扯, 都是隔靴搔痒。

最近刚好把它们俩梳理了一遍, 来写写 move 和 forward 为什么会出现, 他们能解决什么痛点.

太长不看细节

(1) 问题: 临时变量 copy 开销太大

(2) 引入: rvalue, lvalue, rvalue reference概念

(3) 方法: rvalue reference 传临时变量, move 语义避免 copy

(4) 优化: forward 同时能处理 rvalue/lvalue reference 和 const reference

下面是细节了

两个 C++ 的基础背景

  1. C++ 传值默认是 copy
  2. copy 开销很大

C++11 前的状况: 没法避免临时变量的copy

基于以上背景, C++11 以前是没法避免 copy 临时变量的, 如下面例子, 他们都要经历至少一次复制操作:

func("some temporary string");  // 初始化string, 传入函数, 可能会导致 string 的复制

v.push_back(X());               // 初始化了一个临时 X , 然后被复制进了 vector

a = b + c;                      // b+c 是一个临时值, 然后被赋值给了 a

x++;                            // x++ 操作也有临时变量的产生

a = b + c + d;                  // c+d 一个临时变量, b+(c+d)另一个临时变量

(这些临时变量在 C++11 里被定义为 rvalue, 右值, 因为没有对应的变量名存它们)

(同时有对应变量名的被称为 lvalue , 左值)

案例

以上 copy 操作有没有必要呢? 有些地方可不可以省略呢? 我们来看看下面一个案例, 之后我们会用到它来推导出为什么我们需要 move 和 forward :

假如有一个class A, 带有一个 set 函数, 可以传两个参数赋值 class 里的成员变量:

class A{...};

void A::set(const string & var1, const string & var2){
  m_var1 = var1;  //copy
  m_var2 = var2;  //copy
}

下面这个写法是没法避免 copy 的, 因为怎么着都得把外部初始的 string 传进 set 函数, 再复制给成员变量:

A a1;
string var1("string1");
string var2("string2");
a1.set(var1, var2); // OK to copy

但下面这个呢? 临时生成了 2 个 string, 传进 set 函数里, 复制给成员变量, 然后这两个临时 string 再被回收。是不是有点多余?

A a1;
a1.set("temporary str1", "temporary str2"); // temporary, unnecessary copy

上面复制的行为, 在底层的操作很可能是这样的:

(1) 临时变量的内容先被复制一遍

(2) 被复制的内容覆盖到成员变量指向的内存

(3) 临时变量用完了再被回收

这里能不能优化一下呢? 临时变量反正都要被回收, 如果能直接把临时变量的内容, 和成员变量内容交换一下 【其实就是把它俩的内存交换一下,成员变量偷了临时变量的内存,然后丢了自己原来的内存】, 就能避免复制了? 如下:

(1) 成员变量内部的指针指向 "temporary str1" 所在的内存

(2) 临时变量内部的指针指向成员变量以前所指向的内存

(3) 最后临时变量指向的那块内存再被回收

上面这个操作避免了一次 copy 的发生, 其实它就是所谓的 move 语义.

ref: stackoverflow.com/questions/3…

C++11: 引入 rvalue , lvalue 和 move

那么这个临时变量, 在以前是解决不了了. 为了填这个坑, 蛋疼的 C++ 委员会就说, 不如把 C++ 搞得更复杂一些吧!

于是就引入了 rvalue 和 lvalue 的概念, 之前说的那些临时变量就是 rvalue . 上面说的避免 copy 的操作就是std::move 【偷内存】

再回到我们的例子:

没法避免 copy 操作的时候, 还是要用 const T & 把变量传进 set 函数里, 现在 T & 叫 lvalue reference(左值引用) 了, 如下:

void set(const string & var1, const string & var2){
  m_var1 = var1;  //copy
  m_var2 = var2;  //copy
}
A a1;
string var1("string1");
string var2("string2");
a1.set(var1, var2); // OK to copy

传临时变量的时候, 可以传 T && , 叫 rvalue reference(右值引用), 它能接收 rvalue(临时变量), 之后再调用 std::move 就避免copy了.

void set(string && var1, string && var2){
  //avoid unnecessary copy!
  m_var1 = std::move(var1);  
  m_var2 = std::move(var2);
}
A a1;
//temporary, move! no copy!
a1.set("temporary str1","temporary str2");

新的问题: 避免重复

现在终于能处理临时变量了, 但如果按上面那样写, 处理临时变量用右值引用 string && , 处理普通变量用const 引用 const string & ...

这代码量有点大呀? 每次都至少要写两遍, overload 一个新的 method 吗?

回忆一下程序员的核心价值观是什么? 避免重复!

perfect forward (完美转发)

上面说的各种情况, 包括传 const T &T &&, 都可以由以下操作代替:

template<typename T1, typename T2>
void set(T1 && var1, T2 && var2){
  m_var1 = std::forward<T1>(var1);
  m_var2 = std::forward<T2>(var2);
}

// when var1 is an rvalue, std::forward<T1> equals to static_cast<[const] T1 &&>(var1)
// when var1 is an lvalue, std::forward<T1> equals to static_cast<[const] T1 &>(var1)

forward 能转发下面所有的情况:

[const] T &[&]

也就是:

const T &
T &
const T &&
T &&

那么 forward 就是上面一系列操作的集大成者.

如果外面传来了 rvalue 临时变量, 它就转发 rvalue 并且启用 move 语义.

如果外面传来了 lvalue , 它就转发 lvalue 并且启用复制. 然后它也还能保留 const .

这样就能完美转发(perfect forwarding)所有情况了.

那我们有了 forward 为什么还要用 move ?

技术上来说, forward 确实可以替代所有的 move .

可能会有人在这里和我杠吧. 这你得去和 "Effective Modern C ++" 的作者 Scott Meyers 去争了:

"From a purely technical perspective, the answer is yes: std::forward can do it all. std::move isn’t necessary. "
来源:stackoverflow.com/a/28828689

但还有一些问题:

首先, forward 常用于 template 函数中, 使用的时候必须要多带一个 template 参数 T :  forward<T> , 代码略复杂;

还有, 明确只需要 move 的情况而用 forward , 代码意图不清晰, 其他人看着理解起来比较费劲.

更技术上来说, 他们都可以被 static_cast 替代. 为什么不用 static_cast 呢? 也就是为了读着方便易懂.

ref: stackoverflow.com/questions/2…

总结

到这里, move 和 forward 为什么会出现, 有什么用就彻底搞明白了. 其实也就是引入了好几个复杂概念, 来填临时变量的一个坑.

ref: www.open-std.org/jtc1/sc22/w…n2027.html