C++ 0x 之左值与右值、右值引用、移动语义、传导模板

    左值与右值程序员

左值与右值的概念要追溯到 C 语言,由 C++ 语言继承了上来。C++ 03 3.10/1 如是说:“Every expression is either an lvalue or an rvalue.”左值与右值是指表达式的属性,而非对像的属性。express

左值具名,对应指定内存域,可访问;右值不具名,不对应内存域,不可访问。临时对像是右值。左值可处于等号左边,右值只能放在等号右边。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,不然为右值。函数

       注意区分 ++x 与 x++。前者是左值表达式,后者是右值表达式。前者修改自身值,并返回自身;后者先建立一个临时对像,并用 x 的值赋之,后将修改 x 的值,最后返回临时对像。测试

函数的返回值通常状况下是右值,C++ 03 5.2.2/10 如是说:“A function call is an lvalue if and only if the result type is a reference.”好比有 vector<int> v 对像,则 v[0] 即为左值,由于 vector 容器的 [] 算符重载函数的返回值为引用。ui

       左值与右值都可以声明为 const 和 non-const。.net


    拼接字串的问题blog

       上面提到函数返回值通常为右值,也即临时对像。对于内置类型(built-in type)来讲,临时对像仍是可忍的。但对于容器对像来讲就是极大的浪费了。举一个 C++ 98/03 标准下最通俗的例子,拼接字符串继承

0

图一内存

图一第 18 行,短短一句,背后动做极其复杂我要把 string 对像和常量字串交替拼接起来,问题重点在于如何重载 + 算符。有以下几点须要考虑:作用域

  1. 过程当中分别出现 string 对像与常量字串的加法、string 对像与 string 对像的加法。所以须要重载多种 + 算符函数。(加法自左至右)
  2. 好比 string 对像与常量字串的加法,返回的将是一个新生成的 string 对像,所以必须返回这个对像的复本,是临时对像,是右值。又因为加法是连续运算的,下一个加法的重载函数为了接收这一右值,参数表只得写成传值的形式,也即将此临时对像再复制一次,才可传到函数体内操做。总的来讲,就是临时对像由前一个函数体转到另外一个函数体,须要深度复制两次
  3. 由第二点可知,仅仅由一个加号过渡到另外一个加号,就要产生两个昙花一现、转瞬即逝的临时对像复本。若每一个字串都很长,对像都很大,拼接个数又特别多,这要产生多少垃圾?为什么不能把前一个函数返回时产生的临时对像不用复制,直接拿给下一个函数用呢?也就是从前一个函数“移动”到后一个函数体中。
  4. 对于第三点,C++ 98/03 不容许这么作。由于语义上不支持。因为缺少“移动语义”,前一个函数产生的临时对像将在函数体退出时析构,外部要想得到只能使用其复本,本体已经不存在了。

    右值引用和移动语义

       针对上述拼接字串的问题,若说,函数返回时产生的临时对像须要复制出去还情有可原——毕竟人家的做用域到头儿了,本体的确不能传递到外部,只能由复本代劳(这是 C++ 与 C# 最大的不一样之一);不过话又说回来,复本都复制出来了,为什么传递到下一个函数体内还须要再复制一次呢?C++ 98/03 说得是义正词严:

       “由于我规定了,右值不但不能取址,连引用都不能取!谁让丫传的是临时对像,是右值,传参只能传值!”

       话说得多气人呐!凭什么连引用都不能取?传值就意味着深度复制。C++ 标准委员会发现了这一问题,决定在 C++ 0x 新标准中补充“右值引用”和“移动语义”。

       移动语义:将对方掏空,实体吸取给我本身。见《测试 VS 2010 对 C++ 0x 标准的谨慎支持》。

       举一个临时对像由一个函数传往另外一个函数的例子以说明问题。由例子可见,Sck 函数使用右值引用重载版本,接收 Fck 函数返回的临时对像。而在 Fck 函数返回时,完成了一次 Sb 对像的复制。如图:

1

图二

      关于右值引用和移动语义的更多例子,请参见微软 VC 官方博客:《Rvalue Reference》。


    右值引用重载函数几点

  1. 移动构造重载函数和移动赋值算符(assignment operators:=、^=、+=,etc.)重载函数毫不会隐式声明,必须本身定义。
  2. 默认构造函数会被用户本身显式定义的构造函数压制,包括用户自定义复制构造函数和移动构造函数。因故若用户已自定义复制和移动构造函数,且须要无参构造函数时,也须要本身定义。
  3. 隐式复制构造函数会被用户本身显式定义的复制构造函数覆盖,而不是自定义的移动构造函数。
  4. 隐式复制赋值重载函数会被用户本身显式定义的复制赋值重载函数覆盖,而不是自定义的移动赋值重载函数。

       总之一句话,一个类定义完了,程序员嘛也无论,默认构造函数、默认复制构造函数、默认复制赋值函数,编译器都会自动生成。而移动语义的构造函数和赋值函数,则必须由程序员本身显式定义方可以使用。


    操做右值对像实现移动语义

       操做右值对像实现移动语义,须使用 std::move () 方法。不管是对类对像,仍是对类对像的成员变量。使用 move 方法须要引用 <utility> 头文件。详见下例:

2

图三


   外围函数向内部函数准确传参的问题

       见以下代码块:

void Outer ( params ) { Inner ( params ); }

       由 Outer 函数接收参数后,要准确无误地传递给 Inner 函数。所谓的准确无误包括 params 的左、右值属性和 const / non-const 属性此也即“参数传导语义”。实现这一语义的目的是 Inner 函数的类型检查信息能够与 Outer 外部互通,所以由 Outer 到 Inner 之间的参数传导不能对参数属性有任何的改变。

       在 C++ 98/03 标准下,咱们可使用左值引用标识参数类型: T& params;但若我往里传常量呢?常量是右值,传不进去。好,那改为 const T& params 好了,这下左右值均可以传了;但若我要在函数体内修改 params 的值呢?……

       《C++ 0x 之 Lambda:贤妻与娇娃,你娶谁当老婆?听 FP 如何点化 C++》里说:C++ 是万能的。别觉得 C++ 没辙了,我能够重载啊!一种版本我知足不了你的全部要求,我重载出知足你要求的全部版本的函数就行了呗!

       嗯……C++ 果然贤惠!好,我一个参数表有 64 个参数,你把全部版本都重载去吧!估计得有 2^64 个这么多……


    传导模板:forward<>

       话说 C++ 0x 以前的 C++ 在这方面表现得实在是太糙了,简直无法儿看……咱们所期待的完美解决方案是只用一个模板便可处理全部状况,而重载函数再能用也不能这么用。好在 C++ 0x 的 <utility> 头文件中有 forward 模板:

template < typename T > void Outer ( T&& t )
       {
              Inner ( std::forward<T> ( t ) );
       }

       不错,写这么一个就解决了。首先 Outer 函数参数表使用 T&& 类型接收参数。推导过程以下:

  • 若参数 t 为 Type& 型即左值引用,则 T&& 推导为 Type& &&,归化为 Type&,为左值引用。
  • 若参数 t 为 Type&& 型即右值引用,则 T&& 推导为 Type&& &&,归化为 Type&&,为右值引用。
  • 若参数 t 为 const Type&(&&) 型,即常左(右)值引用,则 T&& 推导为 const Type&(&&) &&,归化为 const Type&(&&)。
  • 若参数 t 为值类型,则 T&& 为右值引用,待传值型参数为右值。

       一句话,T&& 模板类型能够保留参数信息

       Outer 使用 T&& 是解释清楚了,那 forward<> 是如何保证由 Outer 到 Inner 的平稳过渡呢?若要知 std::move () 和 std::forward () 是如何实现的,请参见:《C++ 0x 之移动语义和传导模板如何实现》。

相关文章
相关标签/搜索