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

文 / 李博(光宇广贞)

    左值与右值

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

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

       注意区分 ++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 v 对像,则 v[0] 即为左值,因为 vector 容器的 [] 算符重载函数的返回值为引用。

       左值与右值均可以声明为 const 和 non-const。


    拼接字串的问题

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

C++ 0x 之左值与右值、右值引用、移动语义、传导模板_第1张图片

图一

       图一第 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 对像的复制。如图:

C++ 0x 之左值与右值、右值引用、移动语义、传导模板_第2张图片

图二

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


    右值引用重载函数几点

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

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


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

       操作右值对像实现移动语义,须使用 std::move () 方法。无论是对类对像,还是对类对像的成员变量。使用 move 方法需要引用 头文件。详见下例:

C++ 0x 之左值与右值、右值引用、移动语义、传导模板_第3张图片

图三


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

       见如下代码块:

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 的 头文件中有 forward 模板:

       template < typename T > void Outer ( T&& t )
       {
              Inner ( std::forward ( 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 之移动语义和传导模板如何实现》。


    题外话:C++的歧义算符

       参见:《C++ 的歧义算符


       所属分类:C++

       参考:

        递归余七

        人和猴子分配椰子问题

        取球的概率算法

        男人们基本不会作诗

        C语言之#define用法

        .NET中数组的隐秘特性

        如何打印 RichTextBox

        如何打印 DataGridView

        ListView Control 对 Column 进行排序的方法

        文件关联的操作

你可能感兴趣的:(C++)