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() { return holder(); }

holder h;
foo(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;

foo(h2);

因为 h2 是个 lvalue,它的生命周期较长,在赋值语句结束之后,变量仍然存在,还有可能要被别的地方使用。因此,rvalue 的短生命周期给我们提供了在某些情况优化代码的可能。但这种可能在
c++11 以前是没法利用到的,因为我们没法在代码中对 rvalue 区别对待,在函数体中,程序员无法分辨传进来的参数到底是不是 rvalue,缺少一个 rvalue 的标记。回忆一下,T& 指向的是 lvalue,而 const T& 指向的,却可能是
lvalue 或 rvalue,我们没有任何方式能够确认当前参数是否是 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() 是这样一个函数,它接受一个参数,然后返回一个该参数对应的右值引用.

就这么简单!你甚至可以暂时想像它的原型是这样的(当然是错的,正确的原型我们后面再讲)

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,这里面就多做了很多无用功,每一个赋值语句,就有一次资源销毁以及一次拷贝!而事实上我们只是要交换 a 与 b 的内容,中间的拷贝都是额外的负担,完全可以考虑消除这些无用功。

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 当成右值来对待。

二. forward()

1. 转发问题

除了 move() 语义之外,右值引用的提出还解决另一个问题:完善转发 (perfect forwarding),转发问题主要针对的是模板函数,这些函数要处理的是这样一个问题:假设我们有这样一个模板函数,它的作用是:缓存一些
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,因为它有名字 --- 有名字的就是 lvalue.

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

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

2. 两个原则

C++11 引入了右值引用的符号:&&,从前面一路看下来,可能有人已经习惯了一看到 T&& 就以为这是右值引用,这确实很容易误解,T&&  为右值引用是当且仅当 T 为一个具体的类型时才成立,而如果T是推导类型时(如模板参数, auto)这就不一定了,比如说如下代码中的 ref_int,它根据定义必是一个右值引用,但模板函数
func 的参数 arg 则不定是右值引用了,因为此时 T 是一个推导类型。

int&& ref_int;

template <typename T>
void func(T&& arg)
{
}

Scott Meyer 曾对 T&& 这个特殊的东西专门作过一个 talk,他称 T&& 为 universal reference(更新:不久后,c++ 社区认为叫作 forwarding reference 更准确),Universal reference 被实例化后(instantiate),即可能是一个左值引用,也可能是一个右值引用,具体来说,对于推导类型
T,  如果 T&& v  被一个左值初始化,那 v 就是左值引用,如果 v 被右值初始化,那它就是右值引用,很神奇!这是怎么做到的呢?主要来说,在参数推导上,c++ 加入了如下两个原则:

原则 (1):

引用折叠原则 (reference collapsing rule),注意,以下条目中的 T 为具体类型,不是推导类型。

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 被推导为: TYPE* acquire_obj(ARG& &&);

// 根据前面说的折叠原则,我们得到如下原型的函数。
TYPE* 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));
}

注意上面 forward 的原型,这里只给出了参数是左值引用的原型,其实还有一个接受右值引用的重载,另外就是,forward 的模板参数类型 TYPE 与该函数的参数类型并不是直接等价,因此无法根据传入的参数确定模板参数,使用时需要调用方手动去指定模板参数的类型。

下面我们验证一下,上述函数是否能正常工作,假如我们传给 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的原型

显然,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);
} 

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

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

【参考文献】

http://thbecker.net/articles/rvalue_references/section_01.html

https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

分类: c/c++

标签: c++lvaluervalue左值右值c++11

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-08 19:25:54

c++11 中的 move 与 forward的相关文章

(译)C++11中的Move语义和右值引用

郑重声明:本文是笔者网上翻译原文,部分有做添加说明,所有权归原文作者! 地址:http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html C++一直致力于生成快速的程序.不幸的是,直到C++11之前,这里一直有一个降低C++程序速度的顽症:临时变量的创建.有时这些临时变量可以被编译器优化(例如返回值优化),但是这并不总是可行的,通常这会导致高昂的对象复制成本.我说的是怎么回事呢? 让我们

stout代码分析之十:c++11之move和forward

stout中大量使用了c++11的特性,而c++11中move和forward大概是最神奇的特性了. 左值和右值的区别 int a = 0; // a是左值,0是右值 int b = rand(); // b是左值,rand()是右值 直观理解:左值在等号左边,右值在等号右边 深入理解:左值有名称,可根据左值获取其内存地址,而右值没有名称,不能根据右值获取地址. 2. 引用叠加规则 左值引用A&和右值引用A&& 可相互叠加 A& + A& = A& A&am

C++11中的右值引用及move语义编程

C++0x中加入了右值引用,和move函数.右值引用出现之前我们只能用const引用来关联临时对象(右值)(造孽的VS可以用非const引用关联临时对象,请忽略VS),所以我们不能修临时对象的内容,右值引用的出现就让我们可以取得临时对象的控制权,终于可以修改临时对象了!而且书上说配合move函数,可以大大提高现有C++的效率.那么是怎样提高它的效率的呢?看段代码先! #include <iostream> #include <utility> #include <vector

[转载] C++11中的右值引用

C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导规则 特殊模板参数推导规则 解决完美转发问题 引用 在C++98中有左值和右值的概念,不过这两个概念对于很多程序员并不关心,因为不知道这两个概念照样可以写出好程序.在C++11中对右值的概念进行了增强,我个人理解这部分内容是C++11引入的特性中最难以理解的了.该特性的引入至少可以解决C++98中的

C++11中的右值引用

原文出处:http://kuring.me/post/cpp11_right_reference May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导规则 特殊模板参数推导规则 解决完美转发问题 引用 在C++98中有左值和右值的概念,不过这两个概念对于很多程序员并不关心,因为不知道这两个概念照样可以写出好程序.在C++11中对右值的概念进行了增强,我个人理解这部分内容是C

【转载】C++ 11中的右值引用

本篇随笔为转载,原博地址如下:http://www.cnblogs.com/TianFang/archive/2013/01/26/2878356.html 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include <vector>    using namespace std; class obj    {    public :        obj() { cout <&l

C++ 11 中的右值引用

C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream>    #include <vector>    using namespace std; class obj    {    public :        obj() { cout << ">> create obj " << endl; }        obj(c

C++11中智能指针的原理、使用、实现

目录 理解智能指针的原理 智能指针的使用 智能指针的设计和实现 1.智能指针的作用 C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理.程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存.使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存. 理解智能指针需要从下面三个层次: 从较浅的层面看,智能指针是利用了一种叫做RAII(资

C++11 中值得关注的几大变化(网摘)

C++11 中值得关注的几大变化(详解) 原文出处:[陈皓 coolshell] 源文章来自前C++标准委员会的 Danny Kalev 的 The Biggest Changes in C++11 (and Why You Should Care),赖勇浩做了一个中文翻译在这里.所以,我就不翻译了,我在这里仅对文中提到的这些变化"追问为什么要引入这些变化"的一个探讨,只有知道为了什么,用在什么地方,我们才能真正学到这个知识.而以此你可以更深入地了解这些变化.所以,本文不是翻译.因为写