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

TYPE* acquire_obj(ARG arg)

{

static list 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

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(get_holder());

虽然在上面的代码中,我们传递给acquire_obj的是一个rvalue,但是在acuire_obj内部,我们再使用这个参数时,它却永远是lvalue,因为它有名字。

acquire_obj这个函数它的基本功能只是传发一下参数,理想状况下它不应该改变我们传递参数的类型:假如我们传给它lvalue,它就应该传lvalue给TYPE,假如我们传rvalue给它,它就应该传rvalue给TYPE,但上面的写法却没有做到这点,而在c++11以前也没法做到。

forward()函数的出现,就是为了解决这个问题。

forward()函数的作用:它接受一个参数,然后返回该参数本来所对应的类型。

比如说在上述的例子中(暂时省略参数的原型,后面再介绍):

复制代码

holder* h = acquire_obj(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(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

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

TYPE&& forward(typename remove_reference::type& arg)

{

return static_cast(arg);

}

template

TYPE* acquire_obj(ARG&& arg)

{

return new TYPE(forward(arg));

}

复制代码

下面我们验证一下,上述函数是否能正常工作,假如我们传给acquire_obj一个lvalue,根据上面说的模板推导原则,ARG会被推导为ARG&,我们得到如下函数:

复制代码

TYPE* acquire_obj(ARG& && arg)

{

return new TYPE(forward(arg));

}

以及相应的forward()函数。

TYPE& &&

forward(typename remove_reference::type& arg)

{

return static_cast(arg);

}

再根据折叠原则,我们得到如下的函数:

TYPE* acquire_obj(ARG& arg)

{

return new TYPE(forward(arg));

}

以及相应的forward()函数。

TYPE&

forward(typename remove_reference::type& arg)

{

return static_cast(arg);

}

复制代码

所以,最后在acquire_obj中,forward返回了一个lvalue, TYPE的构造函数接受了一个lvaue, 这正是我们所想要的。

而假如我们传递给acquire_obj一个rvalue的参数,根据模板推导原则,我们知道ARG会被推导为ARG,于是得到如下函数:

复制代码

TYPE* acquire_obj(ARG&& arg)

{

return new TYPE(forward(arg));

}

以及相应的forward()函数。

TYPE&&

forward(typename remove_reference::type& arg)

{

return static_cast(arg);

}

复制代码

最后acquire_obj中forward()返回了一个rvalue,TYPE的构造函数接受了一个rvalue,也是我们所想要的。

可见,上面的设计完成了我们所想要的功能,这时的acquire_obj函数才是完美的转发函数。

三.move的原型

复制代码

template

typename remove_reference::type&&

std::move(T&& a)

{

typedef typename remove_reference::type&& RvalRef;

return static_cast(a);

}

复制代码

根据rvalue引用的模板推导原则和折叠原则,我们很容易验证,无论是给move传递了一个lvalue还是rvalue,最终返回的,都是一个rvalue reference.

而这正是move的意义,得到一个rvalue的引用。

看到这里有人也许会发现,其实就是一个cast嘛,确实是这样,直接用static_cast也是能达到同样的效果,只是move更具语义罢了。


手机:18678812288 E-Mail:1069706080@qq.com
地址:山东省济南市舜耕路泉城公园东门园内向北50米 鲁ICP备07011972号 版权所有2008-2013 新利体育18
Baidu