一、右值
左值、右值。顾名思义,就是可以出现在=(赋值运算符)左边和右边的值。看下面几个例子:
int func(){ return 2; }int main(){func() = 2;return 0}// 编译错误:// error: lvalue required as left operand of assignment// =操作符的左边必须是左值。// **********************************************************************int& func(){return 2;}int main(){return 0;}// 编译错误// error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'// 右值赋值给非const引用是不正确的初始化。
- 左值
- 最先在C中定义,“可以出现在=操作符左边的值”。
- 随着C特性的增加,有些特殊的左值不能出现在=操作符的左边。
- 数组类型
- 不完整类型
- const类型
- 若是struct、union,则只要它的任意成员(递归地)含有const。
- 左右值很难有一个精确定义,可以按以下理解:
- 左值是一个表示内存位置的表达式,可以通过&取地址符获得内存地址。
- 右值
- 表达式不是左值,就是右值。也就是说上面的这些特殊的左值也是右值。
- 左值可以代替右值,反过来不行,这很好理解,可以直接去左值内存里的值(就是右值了)。
int var; // 左值var = 4; // 4,右值。4 = var; // 错误,4是右值(var + 1) = 2; // 错误,var+1的值是右值。const int var = 4; // 右值,不可修改的左值是右值。int a = 42;int b = 43;// a、b都是左值a = b; // okb = a; // oka = a * b; // ok// a * b是右值int c = a * b; // ok, rvalue on right hand side of assignmenta * b = 42; // error, rvalue on left hand side of assignment// 左值://int i = 42;i = 43; // ok, i is an lvalueint* p = &i; // ok, i is an lvalueint &foo();foo() = 42; // ok, foo() is an lvalueint* p1 = &foo(); // ok, foo() is an lvalue// 右值://int foobar();int j = 0;j = foobar(); // ok, foobar() is an rvalueint* p2 = &foobar(); // error, cannot take the address of an rvaluej = 42; // ok, 42 is an rvalue
// *****************************************************************// 右值可以用左值代替int a = 1; // a 是左值int b = 2; // b 是左值int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值// + 返回右值string& str = std::string(); // 错误,str是左值引用,string()是右值// *****************************************************************// 左值不能用右值代替int main(){fuck1("asdfasdf"); // 编译错误,右值不能赋值给左值引用fuck2("asdfasdf"); // 编译通过,右值可以赋值给常量的引用。}// 常量引用可以用右值赋值,可以避免创建临时对象。void fuck1(string& a) {return;}void fuck2(const string& a) { // 因此C++中经常使用const类型引用。return;}
二、右值引用
A a;A& ref1 = a; // 左值引用,本来就叫引用,但是为了和右值引用区别,就叫左值引用 。A&& ref2 = a; // 右值引用
右值引用是C++11的特性。右值引用的设计目的是为了解决以下两个问题:
- 实现移动语义(Move Semantics)
- 为程序员提供一种控制函数重载的机制,根据实参是左值/右值类型,来调用对应的重载函数。
- 具体场景:为类提供移动构造/赋值函数,让类既支持对象拷贝又支持对象移动,如std::vector的对象移动可以避免创建临时对象带来的高额开销。
- 完美转发(Perfect Forwarding)
template<typename T>class Base {public:Base() = default;~Base() = default;Base& operator=(const X&); // 拷贝赋值private:T* m_pResources; // 假设m_pResources是一个拷贝代价很大的数据// 比如是一个很大的数组。}template<typename T>Base& Base<T>::operator=(const Base& rhs) {// 以下是拷贝赋值的伪代码:if(&rhs == this) return *this; // 查重T* tmp = copy(rhs.m_pResource); // 拷贝一份rhs的副本clear(m_pResource); // 清除自身的数据m_pResource = tmp; // 绑定到拷贝的副本。return *this;}// *************************************************************************Base<int> v1;......; // 在v1中填充了数据Base<int> v2;v2 = v1; // 这里原本的目的是将v1的数据转移到v2中。// 而实际上触发的是拷贝构造,因此会带来巨大的拷贝开销,大大降低性能。
假如我们是C++的设计者,我们该如何改进?我们可以这样做:再增加两个类似拷贝构造和拷贝赋值的函数,这两个函数负责完成将参数的数据转移到本对象中。如下:
template<typename T>Base<T>::Base(UnknownType rhs) { // 构造函数,将rhs的数据转到this中,避免临时拷贝创建......}// 上面这个函数其实就是移动构造函数,UnknownType其实就是右值引用(X&&)。template<typename T>Base& Base<T>::operator=(UnknownType rhs) {// 以下是将rhs的数据移动到this上的伪代码:if(&rhs == this) return *this; // 查重swap(m_pResource, rhs.m_pResource); // 交换数据,就达到了转移数据的目的。return *this;}// 上面这个函数其实就是移动赋值函数,
何时触发拷贝赋值还是新设计的函数(移动赋值函数)?那就要看赋值运算符(=)的右边是左值还是右值,当是左值时,优先匹配拷贝赋值;当是右值时,优先匹配移动赋值。
还有一个问题,虽然提供了移动接口,但是程序员好像并不能决定使用哪个,因为程序员不能决定一个值是左值还是右值。因此还需要提供一个函数move,它一定会返回一个右值:
int leftVal = 1; // leftVal是一个左值int &&rRef = std::move(leftVal); // 正确,rRef是leftVal的右值引用。// move返回leftVal的右值。int && rRef = leftVal; // 错误,右值引用无法绑定到左值。
至此,程序员终于可以自己决定出发拷贝赋值还是移动赋值。
Base<int> v1;......; // 在v1中填充了数据Base<int> v2;v2 = v1; // 触发拷贝赋值,因为=赋值运算符的右边是左值v2 = std::move(v1); // 触发移动赋值,因为=赋值运算符的右边是右值引用。
注意移动赋值里的swap函数的实现:
// 普通版本的swap,并没有出现右值,因此也就不会触发T的移动构造、移动赋值。template<typename T>void swap(T& a, T& b) {T tmp(a);a = b;b = tmp;}// move版本的swapusing std::move;template<typename T>void swap(T& a, T& b){T tmp(move(a)); // move(a)返回右值,触发T的移动构造函数。a = move(b); // move(b)发回右值,触发移动赋值函数。b = move(tmp); // 触发移动赋值函数。}
右值引用是不是右值?
答:有名字的右值引用是左值,没有名字的右值引用,才是右值,我们可以记忆为if-it-has-a-name rule。见如下代码:
void foo(Base&& x){Base x1 = x; // 触发拷贝构造,x并不是右值。右值引用x的名字是x。......}Base& goo();Base x = goo(); // 触发移动构造,goo()返回值是右值,没有名字。// 为什么要这样?其实这样才合理。因为foo中的x理应在整个函数体内都可以被使用。// 若x此时是右值,而x1=x移动构造,这是一种有潜在破坏性的拷贝,x的数据很有可能已经无效。// 这显然是不合理的,因此才有if-it-has-a-name rule。
当Derived继承Base时,且Derived也有移动构造/赋值,请注意:
class Dervied : public Base {public:......Derived(Derived&& d);Derived& operator=(Derived&& d) noexcept; // 移动构造一般不会报异常。}Dervied::Dervied(Dervied && d):Base(std::move(d)) // 正确,调用Base的移动构造//:Base(d) // 错误,d是左值,调用Base的拷贝构造{}Derived& Dervied::operator=(Derived&& d) noexcept{Base::operator=(d); // 千万别忘了,调用Base的移动赋值。......}
注意!谨慎使用std::move,否则可能事与愿违(性能下降):
Base foo(){Base ret;......return ret; // 触发拷贝构造。return std::move(ret); // 性能可能还会下降。}// ************************************************int main(){auto fuck = foo();// 在现代编译器中,这种代码会被优化,foo并不是返回一个ret的拷贝,// 而是直接在调用处直接构造ret,因此整个过程只触发了一次拷贝构造,// 如果使用return std::move(),反而会阻止编译器的优化。}
2、完美转发(Perfect Forwarding)
考虑以下场景:
template<typename T, typename Arg>T* factory(Arg arg){return new T(arg);}// factory是一个常见的参数转发函数,将ar参数转发给T的构造函数。// 显然这是很失败的factory,因为arg是值传递(多余拷贝),很快我们想到改成引用版本://**********************************************************************************......T* factory(Arg& arg){......}// 这避免了值传递,但是arg的实参只能是左值factory(foo()); // 错误,foo的返回值是右值factory(4); // 错误,4是右值。// 在左右值中知识中,我们知道,const Arg& 是可以赋值右值的,因此我们可以再加一个常量引用版本//**********************************************************************************......T* factory(const Arg& arg){......}// 到目前为止,左右值都可以转发,但是不够完美:// 1、一个参数就有两个版本的factory,代码太多了。// 2、arg始终都是左值,在内部并不能触发移动语义。// 显然还是要靠右值引用。
C++的函数参数类型推导规则,包含两部分:
- 第一,引用折叠规则
- A& & 变成 A&
- A& && 变成 A&
- A&& & 变成 A&
- A&& && 变成 A&&
- 第二,模板参数推导规则(有模板的时候)
- 若实参是A类型的左值,则T推导为A&
- 若实参是A类型的右值,则T推导为A&&,见下面代码:
template <typename T>void test( T &&val ){}void main(){int i = 0;const int ci = i;// 实参是int的左值,T推断为int&,val的类型是int& &&,折叠为int&test( i );// 实参是int的特殊左值,T推断为const int&,val的类型是const int& &&,折叠为const int&test( ci );// 实参是int的右值,T推断为T,val的类型是T&&test( i * ci );return;}
根据以上规则,我们可以设计一个函数,使得传入的是左值类型参数时,以左值引用类型参数传递,当传入的是右值类型参数时,以右值引用类型参数传递,这样的话就可以完美转发左右值参数了,如下设计:
template<typename T>T* factory(Arg&& arg){return new T(std::forward<Arg>(arg));}template<class Arg>Arg&& forward(typename remove_refernece<Arg>::type& a) noexcept{return static_cast<Arg&&>(a);}// ********************************************************************************// 假设实参是左值类型参数,代码:X x;factory<A>(x);// 根据上面的模板参数推导规则,x是X类型的左值,则Arg推断为X&,模板代码变为:template<typename T>T* factory(X& && arg){return new T(std::forward<X&>(arg))}template<class S>X& && forward(typename remove_refernece<X&>::type& a) noexcept{return static_cast<X& &&>(a);}// ***************************************// 根据引用折叠规则,模板代码变为:template<typename T>T* factory(X& arg){return new T(std::forward<X&>(arg));}template<class S>X& forward(X& a) noexcept{return static_cast<X&>(a);}// 参数arg是左值类型,且在factory、forward函数中是以左值引用方式传递,避免了值拷贝// 这是完美的左值类型参数传递(转发)方案。// ********************************************************************************// 假设实参是右值类型参数,代码:X foo();factory<A>(foo());// 实参类型是X&&,则Arg推断为X,模板代码变为:template<typename T>T* factory(X&& arg){return new T(std::forward<X>(arg));}template<class S>X&& forward(X& a) noexcept{return static_cast<X&&>(a);}// 参数类型是右值类型,且在factory、forward中是以右值引用方式传递,会触发转移语义// 这正是完美的右值类型参数传递方案。
综上,这个方案确实就是完美转发方案。
回头再看下std::move的可能实现:
template<class T>typename remove_reference<T>::type&& std::move(T&& a) noexcept{typedef typename remove_reference<T>::type&& RvalRef;return static_cast<RvalRef>(a);}// 通过假设两种场景来验证一下// 场景一:传入左值类型参数// 场景二:传入右值类型参数// *********************************************************************************************// 场景一:假设如下代码:X x;std::move(x); // 此时x是左值,看move如何返回右值引用。// 根据上面的参数类型推导规则,实参是左值类型,T推断为X&,代码变为:template<class T>typename remove_reference<T&>::type&& std::move(T& && a) noexcept{typedef typename remove_reference<T&>::type&& RvalRef;return static_cast<RvalRef>(a);}// 进行引用折叠,去掉remove_reference,得到代码:template<class T>T&& std::move(T& a) noexcept{return static_cast<T&&>(a);}// ****************************************// 场景二:假设代码如下:X foo();std::move(foo());// 同上理推导出代码为:template<class T>T&& std::move(T&& a) noexcept{return static_cast<T&&>(a);}// 这乍一看,直接调用static_cast不就行了嘛,事实是确实可以,为了规范,还是要调用std::move
三、移动语义的except
这里十分建议你遵守以下两个规则:
- 尽力将移动构造函数/移动赋值函数设计成不会抛出异常
- 并将函数声明成noexcept。 ```cpp
template
这样建议的主要原因是,假设是std::vector,如果没有遵守上面两条规则,则std::vector gets resized的时候,不会触发转移语义,即已有元素不会relocated到新的内存块。Scott Meyers的《Effective Modern C++》中有详细说明。<a name="nZQOm"></a># 四、总结```cppvoid foo(T& t){} // t是左值时,匹配此函数void foo(T&& t){ // 当t是右值时,匹配此函数// 当进入到函数内,t就不再是右值,而是左值,因为t有名字(t)。T f = std::move(t); // 需要再触发移动语义,必须显式再调用std::move}T t;foo(t); // 匹配foo(T&)foot(std::move(t)) // 匹配foo(T&&)// std::move返回对象的右值引用。std::forward<T&>(t); // 返回值是左值引用std::forward<T&&>(t); // 返回值是右值引用。std::forward<T>(t); // 返回值是右值引用。// std::move,让程序员可以获得指定对象的右值引用// std::forward,让程序员可以获得指定对象的左值引用或右值引用。
