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

转载至: http://www.dutor.net/index.php/2013/11/rvalue-reference-move-semantics-and-perfect-forwarding/

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

1
2
std::vector<string> v;
v.push_back("string");

  

向 vector 中添加一个元素,这个动作需要先后调用 string::string(const char*), 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 针对这种特殊情况做的规则修订,目的是为了实现『完美转发』(perfect forwarding)。

完美转发

  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_;
};

  在介绍这种转发之前,先需要知道右值引用形参的函数模板的实参推演规则,即引用折叠(reference collapsing)。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 中引入很多特性,大多让人眼前一亮:靠,这就是我一直想要的啊!很多特性浏览一遍就清晰了,但右值引用相关的,尤其是完美转发相对来说比较绕,难以理顺。右值引用有两个应用,最基本的动机是移动语义,同时又给完美转发的支持带来契机。

时间: 2024-10-27 13:51:23

C++ 右值引用:移动语义与完美转发的相关文章

右值引用,转移语义与完美转发

1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& foo()

详解C++右值引用

http://jxq.me/2012/06/06/%E8%AF%91%E8%AF%A6%E8%A7%A3c%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/#thbecker C++0x标准出来很长时间了,引入了很多牛逼的特性[1].其中一个便是右值引用,Thomas Becker的文章[2]很全面的介绍了这个特性,读后有如醍醐灌顶,翻译在此以便深入理解. 目录 概述 move语义 右值引用 强制move语义 右值引用是右值吗? move语义与编译器优化 完美转发:问题

深入右值引用,move语义和完美转发

深入右值引用,move语义和完美转发 转载请注明:http://blog.csdn.net/booirror/article/details/45057689 乍看起来,move语义使得你可以用廉价的move赋值替代昂贵的copy赋值,完美转发使得你可以将传来的任意参数转发给 其他函数,而右值引用使得move语义和完美转发成为可能.然而,慢慢地你发现这不那么简单,你发现std::move并没有move任何东西,完美转发也并不完美,而T&&也不一定就是右值引用-- move语义 最原始的左值

Effective Modern C++:05右值引用、移动语义和完美转发

移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引用是将这两个不相关的语言特性连接起来的底层语言机制,正是它使得移动语义和完美转发成了可能. 23:理解std::move和std::forward std::move并不进行任何移动,std::forward也不进行任何转发.这两者在运行期都无所作为,它们不会生成任何可执行代码.实际上,std::m

[转][c++11]我理解的右值引用、移动语义和完美转发

c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象.所有的具名变量或者对象都是左值,而右值不具名.很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值. 看见书上又将右值分为将亡值和纯右值.纯右值就是c++98标准中右值的概念,

[c++11]右值引用、移动语义和完美转发

c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象.所有的具名变量或者对象都是左值,而右值不具名.很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值. 看见书上又将右值分为:将亡值和纯右值. 纯右值就是c++98标准中右值的概

[C++]右值引用和转移语义

右值引用和转移语义 本文尝试着解释何为右值引用和转移语义以及使用它们具有优势,并提供相关案例分析. 定义 左值和右值 首先我们先来理解一下什么是左值和右值. C/C++语言中可以放在赋值符号左边的变量,左值表示存储在计算机内存的对象,左值相当于地址值.右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的"右值",也就是其代表的真实值,右值相当于数据值. C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量.左值表示存

C++11 标准新特性: 右值引用与转移语义

C++ 的新标准 C++11 已经发布一段时间了.本文介绍了新标准中的一个特性,右值引用和转移语义.这个特性能够使代码更加简洁高效. 查看本系列更多内容 | 3 评论: 李 胜利, 高级开发工程师, IBM 2013 年 7 月 10 日 内容 在 IBM Bluemix 云平台上开发并部署您的下一个应用. 开始您的试用 新特性的目的 右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move

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

1. 按值传递 什么是按值传递? 当一个函数通过值的方式获取它的参数时,就会包含一个拷贝的动作.编译器知道如何去进行拷贝.如果参数是自定义类型,则我们还需要提供拷贝构造函数,或者赋值运算符来进行深拷贝.然而,拷贝是需要代价的.在我们使用STL容器时,就存在大量的拷贝代价.当按值传递参数时,会产生临时对象,浪费宝贵的CPU以及内存资源. 需要找到一个减少不必要拷贝的方法.移动语义就是其中一种. 2. 右值引用 此处介绍右值引用的目的,是为了实现后面的移动语义. 右值引用使得我们可以分辨一个值是左值