XML | HTML | TXT
您当前位置:软件开发 >> 新闻动态 >> 软件开发技术 >> 浏览文章

c++11中的move与forward

一. 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更具语义罢了。


手机:18678812288 E-Mail:1069706080@qq.com
地址:山东省济南市舜耕路泉城公园东门园内向北50米 鲁ICP备07011972号 版权所有2008-2013 山东赢德信息科技有限公司