一. move
关于lvaue和rvalue, 在c++11以前存在一个有趣的现象:T& 指向lvalue, const T&即可以指向lvalue也可以指向rvalue。
但就是没有一种引用类型,可以限制为只指向rvalue.
这乍起来好像也不是很大问题,但事实上这个缺陷在有些时候严重的限制了我们在某些情况下,写出更有效率的代码。
举个粟子,假设我们有一个类,它包含了一些资源:
复制代码
class holder
{
public:
holder()
{
resource_ = new Resource();
}
~holder()
{
delete resource_;
}
holder(const holder& other)
{
resource_ = new Resource(*other.resource_);
}
holder(holder& other)
{
resource_ = new Resource(*other.resource_);
}
holder& operator=(const holder& other)
{
delete resource_;
resource_ = new Resource(*other.resource_);
return *this;
}
holder& operator=(holder& other)
{
delete resource_;
resource_ = new Resource(*other.resource_);
return *this;
}
private:
Resource* resource_;
};
复制代码
这是个RAII的类,构造函数与析构函数分别负责资源的获取与释放,因此也相应处理了拷贝构造函数(copy constructor)和重载赋值操作符(assignment operator)。
现在假设我们这样来使用这个类。
// 假设存在如一个函数,返回值为holder类型
holder get_holder();
holder h;
h = get_holder();
这小段代码的最后一条语句做了3件事情:
1) 销毁h中的资源。
2) 拷由get_holder()返回的资源。
3) 销毁get_holder()返回的资源。
我们显然可以发现这其中做了些不是很有必要的事情,假如我们可以直接交换h中的资源与get_holder()返回的资源,那这样我们可以直接省掉第二步中的拷贝动作了。
而这里之所以交换能达到相同的效果,是因为get_holder()返回的是临时的变量,是个rvalue,它的生命周期通常来说很短,具体在这里,就是赋值语句完成之后,任何人都没法再引用该rvalue,它马上就要被销毁了。
如果是像下面这样的用法,我们显然不可以直接交换两者的资源:
holder h1;
holder h2;
h1 = h2;
因为h2是个lvalue,它的生命周期较长,在赋值语句结束之后,变量还要存在,还有可能要被别的地方使用。
显然,rvalue的短生命周期给我们提供了在某些情况优化代码的可能。
但这种可能在c++11以前是没法利用到的,因为:我们没法在代码中对rvalue区别对待,在函数体中,无法分辨传进来的参数到底是不是rvalue,缺少一个rvalue的标记。
回忆一下 T& 指向的是lvalue,而const T&指向的,却可能是lvalue或rvalue,没法区分!
为了解决这个问题,c++11中引入了一个新的引用类型:T&&
这种引用指向的变量是个rvalue, 有了这个引用类型,我们前面提到的问题就迎刃而解了。
复制代码
class holder
{
public:
holder()
{
resource_ = new Resource();
}
~holder()
{
if (resource_) delete resource_;
}
holder(const holder& other)
{
resource_ = new Resource(*other.resource_);
}
holder(holder& other)
{
resource_ = new Resource(*other.resource_);
}
holder(holder&& other)
{
resource_ = other.resource_;
other.resource_ = NULL;
}
holder& operator=(const holder& other)
{
delete resource_;
resource_ = new Resource(*other.resource_);
return *this;
}
holder& operator=(holder& other)
{
delete resource_;
resource_ = new Resource(*other.resource_);
return *this;
}
holder& operator=(holder&& other)
{
std::swap(resource_, other.resource_);
return *this;
}
private:
Resource* resource_;
};
复制代码
这时我们再写如下代码的时候:
holder h1;
holder h2;
h1 = h2; //调用operator(holder&);
h1 = get_holder(); //调用operator(holder&&)
显然后面的实现是更高效的。
写到里,有的人也许提出问题: T&& ref 指向的是右值,那ref本身是左值还是右值?具体来说就是:
1 holder& operator=(holder&& other)
2 {
3 holder h = other;//这里调用的是operator=(holder&) 还是operator=(holder&&)?
4 return *this;
5 }
这个问题的本质还是怎么区分rvalue?
c++11中对rvalue作了明确的定义:
Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
如果一个变量有名字,它就是lvalue,否则,它就是rvalue。
根据这样的定义,上面的问题中,other是有名字的变量,因此是个lvalue,因此第3行调用的是operator=(holder&).
好了说这么久,一直没说到move(),现在我们来给出定义:
c++11中的move()是这样一个函数,它接受一个参数,然后返回一个该参数对应的rvalue().
就这么简单!你甚至可以暂时想像它的原型是这样的(当然是错的,正确的原型我们后面再讲)
T&& move(T& val);
那么,这样一个move(),它有什么使用呢?用处大了!
前面用到了std::swap()这个函数,回想一下以前我们是怎么想来实现swap的呢?
1 void swap(T& a, T& b)
2 {
3 T tmp = a;
4 a = b;
5 b = tmp;
6 }
想像一下,如果T是我们之前定义的holder,这里面多做了多少无用功啊,每一个赋值语句,就有一次资源销毁,以及一次拷贝!但如果用上了move().
1 void swap(T& a, T& b)
2 {
3 T tmp=move(a);
4 a = move(b);
5 b = move(tmp);
6 }
这样一来,如果holder提供了operator=(T&&)重载, 上述操作就完全只是交换了3次指针,效率大大提升!
move使得程序员在有需要的情况下,能够把lvalue当成rvalue来使用。
二. forward()
1.转发问题
除了move()语义之外,rvalue的提出还为了解决另一个问题:转发(forward).
假设我们有这样一个模板函数,它的作用是:缓存一些object,必要的时候,创建新的。
复制代码
template<class TYPE, class ARG>
TYPE* acquire_obj(ARG arg)
{
static list<TYPE*> caches;
TYPE* ret;
if (!caches.empty())
{
ret = caches.pop_back();
ret->reset(arg);
return ret;
}
ret = new TYPE(arg);
return ret;
}
复制代码
这个模板函数的作用简单来说,就是转发一下参数arg给TYPE的reset()函数和构造函数,除此它就没有再干别的事情,在这个函数当中,我们用了值传递的方式来传递参数,显然是比较低效的,多了次无必要的拷贝。
于是我们准备改成传递引用的方式,同时考虑到要能接受rvalue作为参数,于是改成这样:
template<class TYPE, class ARG>
TYPE* acquire_obj(const ARG& arg)
{
//...
}
这样写其实很不灵活:
1)首行,如果reset() 或TYPE的构造函数不接受const类型的引用,那上述的函数就不能使用了,必须另外提供非const TYPE&的版本,参数一多的话,很麻烦。
2)其次,如果reset()或TYPE的构造函数能够接受rvalue作为参数的话,这个特性在acquire_obj()里头永远也用不上。
其中1)好理解,2)是什么意思?
2)说的是这样的问题,即使TYPE存在TYPE(TYPE&& other)这样的构造函数,它在acquire_obj()中也永远不会被调用,原因是在acquire_obj中,传递给TYPE构造函数的,永远是lvalue.
哪怕外面调用acquire_obj()时,传递的是rvalue。
holder get_holder();
holder* h = acquire_obj<holder, holder>(get_holder());
虽然在上面的代码中,我们传递给acquire_obj的是一个rvalue,但是在acuire_obj内部,我们再使用这个参数时,它却永远是lvalue,因为它有名字。
acquire_obj这个函数它的基本功能只是传发一下参数,理想状况下它不应该改变我们传递参数的类型:假如我们传给它lvalue,它就应该传lvalue给TYPE,假如我们传rvalue给它,它就应该传rvalue给TYPE,但上面的写法却没有做到这点,而在c++11以前也没法做到。
forward()函数的出现,就是为了解决这个问题。
forward()函数的作用:它接受一个参数,然后返回该参数本来所对应的类型。
比如说在上述的例子中(暂时省略参数的原型,后面再介绍):
复制代码
holder* h = acquire_obj<holder, holder>(get_holder());
//假设 acquire_obj()接受了一个rvalue作为参数,在它的内部,
TYPE* acquire_obj(arg)
{
//arg本来是rvalue,如果我们直接引用,它会被当成lvalue来使用。
//但如果我们用forward()处理一下,我们却可以得到它的rvalue版本。
//此处 TYPE的构造函数接受的是一个rvalue。
TYPE* ret = new TYPE(forward(arg));
}
//但如果我们传给acquire_obj()的是一个lvalue,
holder h1;
//acquire_obj接受了lvalue作为参数。
acquire_obj<holder,holder>(h1);
TYPE* acquire_obj(arg)
{
//此处,TYPE的构造函数接受的是一个lvalue。
TYPE* ret = new TYPE(forward(arg));
}
复制代码
2. 二个原则
要理解forward()是怎么实现的,先得说说c++11中关于引用的二个原则。
原则(1):
引用折叠原则(reference collapsing rule)
1) T& &(引用的引用) 被转化成 T&.
2)T&& &(rvalue的引用)被传化成 T&.
3) T& &&(引用作rvalue) 被转化成 T&.
4) T&& && 被转化成 T&&.
原则(2):
对于以rvalue reference作为参数的模板函数,它的参数推导也有一个特殊的原则:
假设函数原型为:
template<class TYPE, class ARG>
TYPE* acquire_obj(ARG&& arg);
1)如果我们传递lvalue给acquire_obj(), ARG就会被推导为ARG&,因此
复制代码
ARG arg;
acquire_obj(arg)中acquire_obj被推导为
acquire_obj(ARG& &&)
根据前面说的折叠原则,acquire_obj(ARG& &&)
最后变成
acquire_obj(ARG&)
复制代码
2)如果我们传递rvalue给acquire_obj(),ARG就会被推导为ARG,因此
acquire_obj(get_arg());
则acquire_obj 被推导为 acquire_obj(ARG&&)
3.结论
有了这两个原则,现在我们可以给出最后acquire_obj的原型,以及forward()的原型。
复制代码
template<class TYPE>
TYPE&& forward(typename remove_reference<TYPE>::type& arg)
{
return static_cast<TYPE&&>(arg);
}
template<class TYPE, class ARG>
TYPE* acquire_obj(ARG&& arg)
{
return new TYPE(forward<ARG>(arg));
}
复制代码
下面我们验证一下,上述函数是否能正常工作,假如我们传给acquire_obj一个lvalue,根据上面说的模板推导原则,ARG会被推导为ARG&,我们得到如下函数:
复制代码
TYPE* acquire_obj(ARG& && arg)
{
return new TYPE(forward<ARG&>(arg));
}
以及相应的forward()函数。
TYPE& &&
forward(typename remove_reference<TYPE&>::type& arg)
{
return static_cast<TYPE& &&>(arg);
}
再根据折叠原则,我们得到如下的函数:
TYPE* acquire_obj(ARG& arg)
{
return new TYPE(forward<ARG&>(arg));
}
以及相应的forward()函数。
TYPE&
forward(typename remove_reference<TYPE&>::type& arg)
{
return static_cast<TYPE&>(arg);
}
复制代码
所以,最后在acquire_obj中,forward返回了一个lvalue, TYPE的构造函数接受了一个lvaue, 这正是我们所想要的。
而假如我们传递给acquire_obj一个rvalue的参数,根据模板推导原则,我们知道ARG会被推导为ARG,于是得到如下函数:
复制代码
TYPE* acquire_obj(ARG&& arg)
{
return new TYPE(forward<ARG>(arg));
}
以及相应的forward()函数。
TYPE&&
forward(typename remove_reference<TYPE>::type& arg)
{
return static_cast<TYPE&&>(arg);
}
复制代码
最后acquire_obj中forward()返回了一个rvalue,TYPE的构造函数接受了一个rvalue,也是我们所想要的。
可见,上面的设计完成了我们所想要的功能,这时的acquire_obj函数才是完美的转发函数。
三.move的原型
复制代码
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
复制代码
根据rvalue引用的模板推导原则和折叠原则,我们很容易验证,无论是给move传递了一个lvalue还是rvalue,最终返回的,都是一个rvalue reference.
而这正是move的意义,得到一个rvalue的引用。
看到这里有人也许会发现,其实就是一个cast嘛,确实是这样,直接用static_cast也是能达到同样的效果,只是move更具语义罢了。