假设X是一个类型,那么X&&称作X的右值引用类型,于是为了区分,我们称X&为X的左值引用类型。
右值引用和左值引用很相似,当然也有一些细微的不同点:当我们重载函数解析的时候,参考下面的例子:
void foo(X& x); void foo(X&& x); X x; X foobar(); foo(x); // 参数是左值,所以调用foo(X&) foo(foobar()); // 参数是右值,所以调用foo(X&&)
//这些都是在编译时刻确定的。
在任何时候,你都可以去实现这两种引用类型的重载,但是在大多数情况下,我会建议你在拷贝和赋值构造的时候使用这种重载比较好。
X& X::operator=(X const & rhs); X& X::operator=(X&& rhs) { // 移动语义:交换this和rhs
return *this; }
拷贝构造同理。
警告:在上面的实现中,交换this和rhs是不够好的,我们会在之后讨论这点。
注意:
如果你实现了
void foo(X&);
但是没实现
void foo(X&&);
那么很明显foo将会调用参数为左值的表达式,参数为右值的情况将不会通过编译。
如果你实现了
void foo(X const &);
但是没有实现
void foo(X&&);
那么无论作用在左值或者右值上,foo都会成功。但是他不会区分左值和右值。
但是如果你实现了
void foo(X&&);
但是没有实现其他两个重载,对于右值来说当然是ok的,但是对左值是行不通的。
之前我们说到的move语义,是对右值进行操作的,但是很可怕的是,你同样可以对左值进行move操作。
最明显的例子就是std中的swap操作。
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}
X a, b;
swap(a, b);
上面操作的表达式均不是右值表达式,而且也没有利用move constructor,但是我们很明确的知道使用移动语义的好处,所以我们当然希望在这里也使用到移动语义。
于是有人来拯救我们了,c++11中有一个函数叫std::move,他的作用就是把他的参数转成右值然后返回。
所以在c++11中的swap是这样子的:
template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
如果T没有实现相应的构造函数(右值引用),那么swap操作将会像之前的行为一样(调用拷贝构造)。
使用move有很多好处:
1.对于那些实现了move语义的操作,你会得到一些性能上的提升。
2.STL容器通常要求元素支持拷贝操作,当然如果我们支持move操作,这也会满足这些容器的要求。
但是:
我们看下面这个操作
a=std::move(b);
如果move的内部实现是一个swap操作,那么a和b的值最终会交换。
所 以这个操作完成后,a所携带的资源并不会被释放,其实a的资源会不会被释放的关键取决于b的生存周期,或者说b所占的资源此时代表了当初那个应该被释放的 资源,然而这个资源什么时候最终会被释放呢,我们无法确定。当然如果说这个释放的时间是没有副作用的,那就皆大欢喜了,但是如果,万一这个资源必须在拷贝 的时候被释放掉,那我们非做不可,而不是做一下简单的swap。
X& X::operator=(X&& rhs)
{
// 提前释放那些有可能产生副作用的资源
// 确保对象目前的状态是可析构的和可拷贝的
// swap操作
return *this;
}