ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

右值和move 与 forward

2020-11-15 18:04:50  阅读:248  来源: 互联网

标签:move 右值 TYPE rvalue 引用 && forward holder


一、左值右值的总结

  再次来写左值右值相关的东西我的内心是十分惴惴不安的,一来这些相关的概念十分不好理解,二来网上相关的文章实在太多了,多少人一看这类题目便大摇其头,三来也怕说不清反而误导了别人,反复纠缠这些似乎无关大雅的语言细节实在也有成为 language lawyer 之嫌。但我还是决定再总结一次,因为这是我一直以来学习新东西的一种方式,只有把学到的东西真正写清楚说明白了才是真的理解了,再者也希望自己的经验总结能帮助到有同样困惑的人。

左值右值

  我们一直说左值右值,从 c++ 的术语角度来看,这其实并不十分准确,确切地说应该是左值表达式,右值表达式:表达式是有值的,值是有类型的,值是动态的,类型是静态的,这是基本的概念。而我们说的左值右值,是对值的一种分类,这两个称呼也是从 c 时代遗传下来的:左值是指能出现在等号左边的值,右值是指只能出现在等号右边的值。简单来说,左值就是我们平时定义的变量,右值就是一些临时变量。但到了 c++11,这个分类被细化扩充了,如下所示[N3690,3.10.1]:

因此:

  一个表达式要么是一个 glvalue(generalized lvalue),要么是一个 rvalue;一个 glvalue 要么是一个 lvalue,要么是一个 xvalue(expiring value);一个 rvalue 要么是一个 xvalue,要么是一个 prvalue (pure rvalue)。

  乍一看好像情况变得好复杂,其实不是,图中所说 lvalue 与 c 时代的 lvalue 几乎表达一个意思(不妨称为纯左值),prvalue 与 c 时代的 rvalue 也几乎表达一个意思,所谓纯右值 (pure rvalue),只是多了一个 xvalue,一个介于纯左传与纯右值之间的奇怪物种。本质上来说,xvalue 是 c 时代的 lvalue,它不是中间变量临时变量之类的没有名字的纯右值,之所以再创造出这样一个新的值类型是因为有些时候,我们希望能够将一个纯左值当成临时变量(纯右值)一样来使用,这种被当成纯右值来使用的左值就是 xvalue,说起来很绕,本质上 xvalue 就是一些从程序逻辑上看要 "过时" 的变量(expiring value),它的名字也正是取义自这里。

xvalue 只能通过两种方式来获得,这两种方式都涉及到将一个左值赋给(转化为)一个右值引用[N3690,3.10.1]:

  1. 强制类型转化为右值引用,如 static_cast<T&&>(t); 该表达式得到一个 xvalue。
  2. 返回类型为右值引用的函数调用,如, T&& fun() { return t; };, 则调用 fun() 时, 返回一个 xvalue。

对于第 2 点,有一个与之类似的写法值得大家注意:如果一个函数的返回类型是左值引用,那么调用这个函数得到的返回值将是一个 lvalue[N3690,3.10.1],之所以特别地说这个事,是因为如果一个函数的返回值不是引用类型,那调用这个函数得到的结果将是一个临时变量,是个右值,而且是纯右值(prvalue),嗯,不要搞昏了。

值与引用

  这是另外两个容易混为一谈的概念,引用在 c++ 里是一个很特别的东西,就我的理解,确切地说引用应是一种类型,和 int, float 等类似,比如说 T& t = v;, 则 t 是一个变量,它的类型是引用,指向一个左值,因此也称为左值引用,所以 t 本身是一个左值,类型是一个左值引用。与此相对应,我们也有右值引用: T&& t2 = fun();,同样的 t2 是一个左值,但它的类型是右值引用。所以,我们平时说的"引用"确切来说应该称作"引用变量"才准确,与"整型变量","字符变量"相对(N3690, 8.5.3.1)。当然引用变量这个说法比较牵强(甚至有些政治不正确),毕竟它和一般变量相比实在太不相似了,比如它一般不占内存,比如对它取地址,得到的不是变量本身的地址,比如定义时必须初始化,且不能再次被赋值等等特殊之处,怎么看都是异类。很多人倾向于把引用与指针并论来理解,它们其实也不一样,虽然实现上基本可以认为引用是一个由编译器自动帮你解引用(deference)的指针

  需要注意的是,左值引用变量只能用左值(lvalue)来初始化,右值引用变量只能用右值(xvalue 及 prvalue)来初始化,唯一的例外是 const 类型的左值引用,它既能接受左值,也能接受右值。值得注意的是,引用对临时变量的生命周期是有影响的,如前面所说,临时变量是右值,当一个临时变量被一个引用(const 左值引用或右值引用)指向时,它的生命周期会被延长[N3690,12.2.5]。

关于右值引用,还有一个很容易让人迷惑的语义需要说一说,写法上定义一个右值引用变量的语法如右所示:some_type&& rv_ref = some_rvalue;,但这里要求 somt_type 必须是一个具体的完整的类型,而不能是模板参数,auto,或 decltype 等需要推导的类型,如果 T 是一个需要推导的类型,则 T&& u_t_ref 称为 universal reference 或 forwarding reference,根据 reference collapsing 原则及右值引用推导原则,u_t_ref 最后既可能是左值引用也可能是右值引用,具体可以参看这里。

move 与 forward

  接下来是很多人特别关心的 std::move() 与 std::forward(),对这俩恰恰想总结的却不多,总的来说,以我的浅见,std::move() 的主要作用是将一个左值转为 xvalue, 它的实现,本质上就是一个 static_cast<>。而 std::forward() 则是用来配合 forwarding reference 实现完美转发,它主要的作用是将一个类型为引用(左值引用或右值引用)的左值(引用这个变量本身),转化为它的类型所对应的值类型(解引用?),这个说法实在是太无法理解太不知所云了,所以我放弃用自己的话来解释了,请参看 cppreference 上的例子。

move 语义

  move 语义是一个大家一定要注意,接受,掌握,并理解好的东西。过去使用 c++ 98/03 我们常说 rule of three,即是:如果一个类定义了析构函数,拷贝构造函数,赋值构造函数之一,那么这三个函数都应该要明确定义,目的是为了确保该类的拷贝语义被正确地处理。

  到了 c++11,这个 rule of three 得改成 rule of five。我们知道,一个类如果定义了拷贝构造函数和赋值构造函数,则我们称它为 copyable 的类。同理,如果一个类定义了 move constructor 和 move assignment operator,那么我们称它为 movable 的类。从使用上来说,右值引用是一个很 tricky 的东西,它的正确使用场合应该只有两个:一个是为自定义类型实现 move 语义,一个是配合 forwarding reference 来实现完美转发。当你考虑写一个以右值引用作为参数类型的一般函数时,往往这是错误的开始,正确的作法是为相应的类型定义 move 语义,具有 move 语义的类型在作为参数传递时,要么直接传值(sink parameter),要么传 const 左值引用(read only),根本不需要右值引用这种 tricky 的东西.[2]

  因此,能够在适当时候为自定义类型实现 move 语义是一个基本素质,就正如以前处理 copy 语义一样(会不会将一个类继承自 boost::noncopyable 也是基本素质)。STL 中所有的容器算法都妥善定义了对适用类型 move 语义的要求,如只适用于 copyable,或只适用于 movable 等,容器本身更都是 movable 的。一般来说,move 是一个更轻量的操作,对容器其实更友好(内部 copy 可以改为 move,效率更高),比如 vector<>,以前要求其所保存类型必须 copyable,现在 c++11 以后,只 movable(且 noexcept)的类型也被允许了(当然此时用户就不能调用那些需要 copy 的操作了)。所以,明确定义好自定义类型的 move 语义,意义是很大的。

二、move和forward详解

1. move

  关于 lvaue 和 rvalue,在 c++11 以前存在一个有趣的现象:T&  指向 lvalue (左传引用), const T& 既可以指向 lvalue 也可以指向 rvalue。但却没有一种引用类型,可以限制为只指向 rvalue。这乍看起来好像也不是很大的问题,但实际与看起来不一样,右值引用的缺失有时严重限制了我们在某些情况下,写出更高效的代码。举个粟子,假设我们有一个类,它包含了一些资源:

 1 class holder
 2 {
 3      public:
 4  
 5           holder()
 6           {
 7                resource_ = new Resource();
 8           }
 9           ~holder()
10           {
11                delete resource_;
12           }
13 
14           holder(const holder& other)
15           {
16                 resource_ = new Resource(*other.resource_);
17           }
18 
19           holder(holder& other)
20           {
21                 resource_ = new Resource(*other.resource_);
22           }
23 
24           holder& operator=(const holder& other)
25           {
26                 delete resource_;
27                 resource_ = new Resource(*other.resource_);                return *this;
28           }
29            holder& operator=(holder& other)
30           {
31                 delete resource_;
32                 resource_ = new Resource(*other.resource_);                return *this;
33           }
34           private:
35 
36                Resource* resource_;
37 };

  这是个 RAII 类,构造函数与析构函数分别负责资源的获取与释放,因此也相应处理了拷贝构造函数 (copy constructor) 和重载赋值操作符 (assignment operator),现在假设我们这样来使用这个类。

1 // 假设存在如下一个函数,返回值为 holder 类型的临时变量
2 holder get_holder() { return holder(); }
3 
4 holder h;
5 foo(h);
6 h = get_holder();

  理想情况下(不考虑返回值优化等因素),这一小段代码的最后一条语句做了如下三件事情:

    1)  销毁 h 中的资源。

    2)  拷由 get_holder() 返回的资源。

    3)  销毁 get_holder() 返回的资源。

  显然我们可以发现这些事情中有些是不必要的:假如我们可以直接交换 h 中的资源与 get_holder() 返回的对象中的资源,那我们就可以直接省略掉第二步中的拷贝动作了。而这里之所以交换能达到相同的效果,是因为 get_holder() 返回的是临时的变量,是个 rvalue,它的生命周期通常来说很短,具体在这里,就是赋值语句完成之后,任何人都没法再引用该 rvalue,它马上就要被销毁了,它所包含的资源也无法再被访问。而如果是像下面这样的用法,我们显然不可以直接交换两者的资源:

1 holder h1;
2 holder h2;
3 
4 h1 = h2;
5 
6 foo(h2);

  因为 h2 是个 lvalue,它的生命周期较长,在赋值语句结束之后,变量仍然存在,还有可能要被别的地方使用。因此,rvalue 的短生命周期给我们提供了在某些情况优化代码的可能。但这种可能在 c++11 以前是没法利用到的,因为我们没法在代码中对 rvalue 区别对待:在函数体中,程序员无法分辨传进来的参数到底是不是 rvalue,我们缺少一个 rvalue 的标记。

  回忆一下,T& 指向的是 lvalue,而 const T& 指向的,却可能是 lvalue 或 rvalue,我们没有任何方式能够确认当前参数是不是 rvalue!为了解决这个问题,c++11 中引入了一个新的引用类型: some_type_t &&,这种引用指向的变量是个 rvalue, 有了这个引用类型,我们前面提到的问题就迎刃而解了。

 1 class holder
 2 {
 3      public:
 4  
 5           holder()
 6           {
 7                resource_ = new Resource();
 8           }
 9           ~holder()
10           {
11                if (resource_) delete resource_;
12           }
13 
14           holder(const holder& other)
15           {
16                 resource_ = new Resource(*other.resource_);
17           }
18 
19           holder(holder& other)
20           {
21                 resource_ = new Resource(*other.resource_);
22           }
23           
24           holder(holder&& other)
25           {
26                 resource_ = other.resource_;
27                 other.resource_ = NULL;
28           }
29 
30           holder& operator=(const holder& other)
31           {
32                 delete resource_;
33                 resource_ = new Resource(*other.resource_);          return *this;
34           }
35 
36           holder& operator=(holder& other)
37           {
38                 delete resource_;
39                 resource_ = new Resource(*other.resource_);                return *this;
40           }
41 
42           holder& operator=(holder&& other)
43           {
44                 std::swap(resource_, other.resource_);                return *this;
45           }
46 
47           private:
48 
49                Resource* resource_;
50 };

  因为有了右值引用,当我们再写如下代码的时候:

1 holder h1;
2 holder h2;
3 
4 h1 = h2; // 调用operator(holder&);
5 h1 = get_holder(); // 调用operator(holder&&)

  编译器就能根据当前参数的类型选择相应的函数,显然后者的实现是更高效的。写到里,有的人也许会有疑问:  some_type_t&& ref  指向的是右值(右值引用),那 ref 本身在函数内是左值还是右值?具体来说就是如下代码中,第三行所调用的是 operator=(holder&) 还是 operator=(holder&&)?

1 holder& operator=(holder&& other)
2  {
3        holder h = other;4       return *this;
4  }

  这个问题的本质还是怎么区分 rvalue? c++11 中对 rvalue 作了明确的定义:

1 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(),现在我们来给出它的定义:

1 c++11 中的 move() 是这样一个函数,它接受一个参数,然后返回一个该参数对应的右值引用.

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

1 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&&) 重载,上述操作就相当于只是交换了三次指针,效率大大提升!move() 使得程序员在有需要的情况下能把 lvalue 当成右值来对待。

2. forward()

1. 转发问题

  除了 move() 语义之外,右值引用的提出还解决另一个问题:完美转发 (perfect forwarding),转发问题针对的是模板函数,这些函数主要处理的是这样一个问题:假设我们有这样一个模板函数,它的作用是:缓存一些 object,必要的时候创建新的。

 1 template<class TYPE, class ARG>
 2 TYPE* acquire_obj(ARG arg)
 3 {
 4      static list<TYPE*> caches;
 5      TYPE* ret;
 6 
 7      if (!caches.empty())
 8      {
 9           ret = caches.pop_back();
10           ret->reset(arg);
11           return ret;
12      }
13 
14      ret = new TYPE(arg);
15      return ret;
16 }

  这个模板函数的作用简单来说,就是转发一下参数 arg 给 TYPE 的 reset() 函数和构造函数,除此它就没再干别的事情,在这个函数当中,我们用了值传递的方式来传递参数,显然是比较低效的,多了次没必要的拷贝,于是我们准备改成传递引用的方式,同时考虑到要能接受 rvalue 作为参数,最后做出艰难的决定改成如下样子:

1 template<class TYPE, class ARG>
2 TYPE* acquire_obj(const ARG& arg)
3 {
4     //...
5 }

  但这样写很不灵活:

    1) 首先,如果 reset() 或 TYPE 的构造函数不接受 const 类型的引用,那上述的函数就不能使用了,必须另外提供非 const TYPE& 的版本,参数一多的话,很麻烦。

    2) 其次,如果 reset() 或 TYPE 的构造函数能够接受 rvalue 作为参数的话,这个特性在 acquire_obj() 里头也永远用不上。其中1) 好理解,2) 是什么意思?

    3) 说的是这样的问题,即使 TYPE 存在 TYPE(TYPE&& other) 这样的构造函数,它在上述 acquire_obj() 中也永远不会被调用,原因是在 acquire_obj() 中,传递给 TYPE 构造函数的,永远是 lvalue(因为 arg 有名字),哪怕外面调用 acquire_obj() 时,用户传递进来的是 rvalue,请看如下示例:

1 holder get_holder();
2 
3 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 是一个推导类型。

1 int&& ref_int = get_int();
2 
3 template <typename T>
4 void func(T&& arg)
5 {
6 }

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

  原则 (1):

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

    1)  T& & (引用的引用) 被转化成 T&.

    2)T&& & (rvalue的引用)被传化成 T&.

    3)  T& && (引用作rvalue) 被转化成 T&.

    4)  T&& && 被转化成 T&&.

  原则 (2):

    对于以 rvalue reference 作为参数的模板函数,它的参数推导也有一个特殊的原则,假设函数原型为:

1 template<class TYPE, class ARG>
2 TYPE* acquire_obj(ARG&& arg);

  1) 如果我们传递 lvalue 给 acquire_obj(),则 ARG 就会被推导为 ARG&,因此如下代码的第二行,acquire_obj 被推导为: TYPE* acquire_obj(ARG& &&)。

1 ARG arg;
2 acquire_obj(arg);

  然后根据前面说的折叠原则,我们得到原型如下的函数: TYPE* acquire_obj(ARG&);

  2) 如果我们如下这样传递 rvalue 给 acquire_obj(),则 ARG 就会被推导为 ARG。

acquire_obj(get_arg()); 

  最后,模板函数实例化为原型如下的函数:TYPE* acquire_obj(ARG&&); 

  综上讨论可见,原则 2 其实是有些令人讨厌的,它与一般模板函数的参数类型推导并不一致,甚至可以说有些相背(主要在于 top level cv removal principle),这些随处可见的例外增加了语言的复杂性,加大了学习和记忆的难度,是如此令人讨厌,但在 c++ 中这种现象又那么常见,真是无奈。

3.结论

有了以上两个原则,现在我们可以给出理想的 acquire_obj() 原型,以及 forward() 原型。

 1 template<class TYPE>
 2 TYPE&& forward(typename remove_reference<TYPE>::type& arg)
 3 {
 4    return static_cast<TYPE&&>(arg);
 5 }
 6 
 7 template<class TYPE, class ARG>
 8 TYPE* acquire_obj(ARG&& arg)
 9 {
10    return new TYPE(forward<ARG>(arg));
11 }

  注意上面 forward 的原型,这里只给出了参数是左值引用的原型,其实还有一个接受右值引用的重载(用来处理传入的参数是右值的情况)。另外需要额外注意的是,forward 的模板参数类型 TYPE 与该函数的参数类型并不直接等价,因此无法根据传入的参数推导模板参数,使得调用方必需显式地指定模板参数的类型,如: forward<ARG>(xx),否则会有编译错误。

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

 1 TYPE* acquire_obj(ARG& && arg)
 2 {
 3    return new TYPE(forward<ARG&>(arg));
 4 }
 5 
 6 以及相应的 forward()函数。
 7 
 8 TYPE& && 
 9 forward(typename remove_reference<TYPE&>::type& arg)
10 {
11    return static_cast<TYPE& &&>(arg);
12 }
13 
14 再根据折叠原则,我们得到如下的函数:
15 TYPE* acquire_obj(ARG& arg)
16 {
17    return new TYPE(forward<ARG&>(arg));
18 }
19 
20 以及相应的forward()函数。
21 
22 TYPE& forward(typename remove_reference<TYPE&>::type& arg)
23 {
24    return static_cast<TYPE&>(arg);
25 }

   所以,最后在 acquire_obj 中,forward 返回了一个 lvalue 引用, TYPE 的构造函数接受了一个 lvaue 引用, 这正是我们所想要的。 而假如我们传递给 acquire_obj 一个 rvalue 的参数,根据模板推导原则,我们知道 ARG 会被推导为 ARG,于是得到如下函数:

 1 TYPE* acquire_obj(ARG&& arg)
 2 {
 3    return new TYPE(forward<ARG>(arg));
 4 }
 5 
 6 以及相应的 forward() 函数。
 7 
 8 TYPE&& forward(typename remove_reference<TYPE>::type& arg)
 9 {
10    return static_cast<TYPE&&>(arg);
11 }

  最后 acquire_obj() 中 forward() 返回了一个 rvalue reference,TYPE 的构造函数接受了一个 rvalue,也是我们所想要的。可见,上面的设计完成了我们所想要的功能,这时的 acquire_obj() 函数才是完美的转发函数。

3.move的原型

  显然,move() 必定是一个模板函数,它的参数类型推导完全遵循前面提到两个原则,这就是为何我把它的原型放到现在才写出来,用心良苦啊。

1 template<class T> 
2 typename remove_reference<T>::type&&
3 std::move(T&& a)
4 {
5   typedef typename remove_reference<T>::type&& RvalRef;
6   return static_cast<RvalRef>(a);
7 }

  根据模板推导原则和折叠原则,我们很容易验证,无论是给 move 传递了一个 lvalue 还是 rvalue,最终返回的,都是一个rvalue reference。而这正是 move 的意义,得到一个 rvalue 的引用。看到这里有人也许会发现,其实就是一个 cast 嘛,确实是这样,直接用 static_cast 也是能达到同样的效果,只是 move 更具语义罢了。

参考:https://www.cnblogs.com/catch/p/3507883.html

   https://www.cnblogs.com/catch/p/5019402.html

标签:move,右值,TYPE,rvalue,引用,&&,forward,holder
来源: https://www.cnblogs.com/MrLiuZF/p/13977631.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有