[C++] 右值引用:移动语义与完美转发(C++是一种扼杀生命的语言)

转载 2013年12月04日 16:21:29

[C++] 右值引用:移动语义与完美转发

C++11 引入的新特性中,除了并发内存模型和相关设施,这些高帅富之外,最引人入胜且接地气的特性就要属『右值引用』了(rvalue reference)。加入右值引用的动机在于效率:减少不必要的资源拷贝。考虑下面的程序:

1

2

std::vector<string> v;

v.push_back("string");

  向 vector 中添加一个元素,这个动作需要先后调用 string::string(constchar*), string::string(const string&), string::~string() 三个函数,涉及两次内存拷贝:第一次使用字面常量 “string” 构造出一个临时对象,第二次使用该临时对象构造出 vector 中的一个新元素,『最后临时对象会发生析构』。

移动语义

  上面程序操作的问题症结在于,临时对象的构造和析构带来了不必要的资源拷贝。如果有一种机制,可以在语法层面识别出临时对象,在使用临时对象构造新对象(拷贝构造)的时候,将临时对象所持有的资源『转移』到新的对象中,就能消除这种不必要的拷贝。这种语法机制就是『右值引用』,相对地,传统的引用被称为『左值引用』。左值引用使用 ‘&’ 标识(比如 string&),右值引用使用 ‘&&’ 标识(比如 string&&)。顺带提一下什么是左值(lvalue)什么是(rvalue):可以取地址的具名对象是左值;无法取值的对象是右值,包括匿名的临时对象和所有字面值(literal value)。
  有了右值的语法支持,为了实现移动语义,需要相应类以右值为参数重载传统的拷贝构造函数和赋值操作符,毕竟哪些资源可以移动、哪些只能拷贝只有类的实现者才知道。对于移动语义的拷贝『构造』,一般流程是将源对象的资源绑定到目的对象,然后解除源对象对资源的绑定;对于赋值操作,一般流程是,首先销毁目的对象所持有的资源,然后改变资源的绑定。另外,当然,与传统的构造和赋值相似,还要考虑到构造的异常安全和自赋值情况。作为演示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class String {

public:

    String(const String &rhs) { ... }

    String(String &&rhs) {

        s_ = rhs.s_;

        rhs.s_ = NULL;

    }

    String& operator=(const String &rhs) { ... }

    String& operator=(String &&rhs) {

        if (this != &rhs) {

            delete [] s_;

            s_ = rhs.s_;

            rhs.s_ = NULL;

        }

        return *this;

    }

private:

    char *s_;

};

  值得注意的是,一个绑定到右值的右值引用是『左值』,因为它是有名字的。考虑:

1

2

3

4

5

6

7

8

9

10

11

class B {

public:

    B(const B&) {}

    B(B&&) {}

};

class D : public B {

    D(const D &rhs) : B(rhs) {}

    D(D &&rhs) : B(rhs) {}

};

D getD();

D d(getD());

  上面程序中,B::B(B&&) 不会被调用。为此,C++11 中引入 std::move(T&&t) 模板函数,它 t 转换为右值:

1

2

3

class D : public B {

    D(D &&rhs) : B(std::move(rhs)) {}

};

std::move 的一种可能的实现:

1

2

3

4

5

template <typename T>

typename remove_reference<T>::type&&

move(T &&t) {

    return static_cast<remove_reference<T>::type&&>(t);

}

绑定规则

  引入右值引用后,『引用』到『值』的绑定规则也得到扩充:

  1. 左值引用可以绑定到左值: int x; int &xr = x;
  2. 非常量左值引用不可以绑定到右值: int &r = 0;
  3. 常量左值引用可以绑定到左值和右值:int x; const int &cxr = x; const int &cr = 0;
  4. 右值引用可以绑定到右值:int &&r = 0;
  5. 右值引用不可以绑定到左值:int x; int &&xr = x;
  6. 常量右值引用没有现实意义(毕竟右值引用的初衷在于移动语义,而移动就意味着『修改』)。

  其中,第五条规则『不适用于』函数模板的形参,例如下面的函数可以接受任意类型的参数,既可以是右值也可以是左值,还可以是常量或者非常量:

1

2

3

4

5

6

7

template <typename T>

void foo(T &&t);

int x;

const int xx;

foo(x); //~ OK

foo(xx); //~ OK

foo(10); //~ OK

T&& 形参可以接受左值,是 C++11 针对这种特殊情况做的规则修订,目的是为了实现『完美转发』(perfectforwarding)。

完美转发

C++11 之前,一直存在着参数『转发』的问题,即不能方便地实现完美转发。转发的目的在于传递『引用参数』的附加属性,比如 cv属性(const/volatile)和左右值属性。为了刻画这个问题,我们以左右值属性的传递为例(cv 属性也存在相似的问题),参考下面的类定义:

1

2

3

4

5

6

7

8

class X

{

public:

    X(const std::string &s, const std::vector<int> &v) : s_(s), v_(v) {}

private:

    std::string s_;

    std::vector<int> v_;

};

  为了支持移动语义,就需要重载构造函数,由于构造函数有两个参数,还需要考虑到右值引用和左值引用的组合形式:

1

2

3

4

5

6

7

8

9

10

11

class X

{

public:

    X(const std::string &s, const std::vector<int> &v) : s_(s), v_(v) {}

    X(std::string &&s, const std::vector<int> &v) : s_(std::move(s)), v_(v) {}

    X(const std::string &s, std::vector<int> &&v) : s_(s), v_(std::move(v)) {}

    X(std::string &&s, std::vector<int> &&v) : s_(std::move(s)), v_(std::move(v)) {}

private:

    std::string s_;

    std::vector<int> v_;

};

  如果构造函数有 n 个参数,就需要 2^n 个重载!
C++11 中,通过基于右值引用的函数模板解决了这个问题,本质上是通过对实参类型的推演,按照实际情况,由编译器完成自动的『重载』。

1

2

3

4

5

6

7

8

9

class X

{

public:

    template <typename T1, typename T2>

    X(T1 &&s, T2 &&v) : s_(std::forward<T1>(s)), v_(std::forward<T2>(v)) {}

private:

    std::string s_;

    std::vector<int> v_;

};

  在介绍这种转发之前,先需要知道右值引用形参的函数模板的实参推演规则,即引用折叠(referencecollapsing)。BTW. C++11 之前,不允许绑定到引用的引用类型(reference to reference)。
  设 T 为模板的类型参数,A 为实参的基本类型,则有:

T

形参

折叠后的T

折叠后实参类型

A&

T&

A

A&

A&

T&&

A&

A&

A&&

T&

A&

A&

A&&

T&&

A

A&&

  可以看到,当函数的形参声明为 T&& 时,当且仅当实参为右值或者右值引用,折叠后的的实参类型才是右值引用,否则为左值引用。通过这个折叠规则,就可以实现左右值引用属性的转发。std::forward 就可以简单地实现为:

1

2

3

4

5

template <typename T>

T&& forward(T &&t)

{

    return static_cast<T&&>(t);

}

总结

C++11 中引入很多特性,大多让人眼前一亮:靠,这就是我一直想要的啊!很多特性浏览一遍就清晰了,但右值引用相关的,尤其是完美转发相对来说比较绕,难以理顺。右值引用有两个应用,最基本的动机是移动语义,同时又给完美转发的支持带来契机。

 

相关文章推荐

[C++] 右值引用:移动语义与完美转发

C++11 引入的新特性中,除了并发内存模型和相关设施,这些高帅富之外,最引人入胜且接地气的特性就要属『右值引用』了(rvalue reference)。加入右值引用的动机在于效率:减少不必要的资源拷...

C++11右值引用:移动语义和完美转发

转自:http://blog.csdn.net/zwvista/article/details/12306283 很详细的一篇blog 右值引用 为了解决移动语义及完美转发问题,C++11标准引...

C++11线程指南(4)--右值引用与移动语义

1. 按值传递   什么是按值传递?   当一个函数通过值的方式获取它的参数时,就包含有一个拷贝的动作。编译器知道如何去进行拷贝。如果参数是自定义类型,则我们还需要提供拷贝构造函数,或者赋值运算符来进...

C++的杂七杂八:我家的返回值才不可能这么傲娇(右值引用和移动语义)

大凡编程语言,都会有“函数”这个概念。而对于外部而言,一个函数最重要的部分就是它的返回值了。 直接通过返回值将函数的计算结果返回出去,不论是函数还是调用者都会身心愉悦,因为这是最自然的使用方法了。但是...

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

转自http://blog.csdn.net/hikaliv/archive/2009/09/11/4541429.aspx 文章已同步到山里来的鱼和GAE博客左值与右值左值与右值的概念要追溯到 C ...

C++11:深入理解右值引用,move语义和完美转发

乍看起来,move语义使得你可以用廉价的move赋值替代昂贵的copy赋值,完美转发使得你可以将传来的任意参数转发给 其他函数,而右值引用使得move语义和完美转发成为可能。然而,慢慢地你发现这不那么...

C++11新特性:移动语义和右值引用

右值引用传统的C++引用(左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可以获得其地址。 C++11新增了右值引用。右值引用,顾名思义,可以关联到右值,...

C++新特性 右值引用 移动构造函数

1、右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题。但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision、RVO(...

C++11移动语义探讨——从临时对象到右值引用

一.前言这篇文章主要谈谈c++11中引入的右值引用概念和移动语义概念。以及这些东西可能在我们编程中带来哪些体验、便捷或者是代码效率的提高。 文章主要分为以下三点: 临时对象的产生 何谓右值引用 何谓...

C++11特性--右值引用,移动语义,强制移动move()

1.右值引用   *右值:不能对其应用地址运算符的值。   *将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址   *右值包括字面常量(C风格字符串除外,它表示地址),诸...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)