简单记忆
move避免复制,forward避免重载(遇到右值move,遇到左值复制)
std::move
std::move函数可以以非常简单的方式将左值引用转换为右值引用。(左值、左值引用、右值、右值引用 参见:
左值和右值:【C/C++】理解C和C++中的左值和右值_bandaoyu的博客-CSDN博客
不正确但便于理解的解释:(左值引用转换为右值引用:b = 2;a = b; b是左值,有地址。 如果b是临时变量,用完就放弃,那就用move将他变成右值,同时它的地址赋予a,就避免了拷贝操作。a = move(b))
通过std::move,可以避免不必要的拷贝操作。
std::move是为性能而生。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
如string类在赋值或者拷贝构造函数中会声明char数组来存放数据,然后原string中的 char 数组被析构函数释放,如果a是一个临时变量,则上面的拷贝,析构就是多余的,完全可以把临时变量a中的数据直接 “转移” 到新的变量下面即可。
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
move和forward虽然是8年前C++提出的新东西, 但要搞懂还是得费一些精力.
太长不看细节: TL;DR:
(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语义.
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吗?
回忆一下程序员的核心价值观是什么? 避免重复!
std::forward
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.
但还有一些问题:
首先, forward常用于template函数中, 使用的时候必须要多带一个template参数T: forward<T>
, 代码略复杂;
还有, 明确只需要move的情况而用forward, 代码意图不清晰, 其他人看着理解起来比较费劲.
更技术上来说, 他们都可以被static_cast替代. 为什么不用static_cast呢? 也就是为了读着方便易懂.
总结
到这里, move和forward为什么会出现, 有什么用就彻底搞明白了. 其实也就是引入了好几个复杂概念, 来填临时变量的一个坑.
理解C和C++中的左值和右值
简单定义
lvalue(locator value)代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。
rvalue rvalue是不在内存中占有确定位置的表达式。
左值:有址值
右值:无址值 (只是计算的周期驻留在临时的寄存器中)
基本例子
int var;
var = 4;
赋值运算符要求一个lvalue作为它的左操作数,var是一个左值,因为它是一个占确定内存空间的对象。另外下面的代码是无效的:
4 = var; //ERROR!
(var + 10) = 4; //ERROR!
常量4
和表达式var+1
都不是lvalue(它们是rvalue)。因为都是表达式的临时结果,没有确定的内存空间(它们只是计算的周期驻留在临时的寄存器中)。因此给它们赋值没有语意-这里没有地方给它们赋值。
int foo() { return 2; }
int main()
{
foo() = 2;
return 0;
}
foo
返回一个临时的rvalue。尝试给它赋值,foo()=2
,是一个错误;编译器期待在赋值运算符的左部分看到一个lvalue。
不是所有的对函数调用结果赋值都是无效的。比如,C++的引用(reference)让这成为可能:
int globalvar = 20;
int& foo()
{
return globalvar;
}
int main()
{
foo() = 10;
return 0;
}
这里foo
返回一个引用,这是一个左值,所以它可以被赋值。C++从函数中返回左值的能力对于实现一些重载运算符时很重要的。一个普遍的例子是在类中为实现某种查找访问而重载中括号运算符 []。std::map
可以这样做。
std::map<int, float> mymap;
mymap[10]=5.6;
给 mymap[10] 赋值是合法的因为非const的重载运算符 std::map::operator[] 返回一个可以被赋值的引用。
‘&’ 符号在C++中扮演了另一个重要角色-它允许定义 引用类型。这被称为“左值引用”。非const左值引用不能被赋右值,因为这将要求一个无效的右值到左值的转换:
std::string& sref = std::string(); //错误:无效的初始化,
//用一个右值类型‘std::string’初始化非const引用类型‘std::string&’
- 常量左值引用可以被赋右值。因为它们是常量,不能通过引用被修改,因此修改一个右值没问题。这使得C++中接受常量引用作为函数形参成为可能,这避免了一些不必要的临时对象的拷贝和构造。