原文地址:minmin的C++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++ 的基础背景
- C++ 传值默认是 copy
- 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 为什么会出现, 有什么用就彻底搞明白了. 其实也就是引入了好几个复杂概念, 来填临时变量的一个坑.