前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++11——对象移动与右值引用

C++11——对象移动与右值引用

作者头像
恋喵大鲤鱼
发布2018-08-03 11:40:50
8180
发布2018-08-03 11:40:50
举报
文章被收录于专栏:C/C++基础C/C++基础

1.对象移动

C++11新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力。如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能。参考如下程序:

代码语言:javascript
复制
//moveobj.cpp

#include <iostream>
#include <vector>
using namespace std;

class Obj{
public:
    Obj(){cout <<"create obj" << endl;}
    Obj(const Obj& other){cout<<"copy create obj"<<endl;}
};

vector<Obj> foo(){
     vector<Obj> c;
     c.push_back(Obj());
     cout<<"---- exit foo ----"<<endl;
     return c;
}

int main(){
    vector<Obj> v;
    v=foo();
    getchar();
}

编译并运行:

代码语言:javascript
复制
[b3335@localhost test]$ g++ moveobj.cpp
[b3335@localhost test]$ ./a.out 
create obj
copy create obj
---- exit foo ----
copy create obj

可见,对obj对象执行了两次拷贝构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机: (1)第一次是在函数foo中通过临时Obj的对象Obj()构造一个Obj对象并入vector中; (2)第二次是通过从函数foo中返回的临时的vector对象来给v赋值时发生了元素的拷贝。

由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。

代码语言:javascript
复制
[b3335@localhost test]$ g++ -std=c++11 moveobj.cpp 
[b3335@localhost test]$ ./a.out 
create obj
copy create obj
---- exit foo ----

可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?

在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下: (1)foo()函数返回一个临时对象(这里用~tmp来标识它); (2)执行vector的 ‘=’ 函数,将对象v中的现有成员删除,将~tmp的成员复制到v中来; (3)删除临时对象~tmp。

在C++11的版本中,执行过程如下: (1)foo()函数返回一个临时对象(这里用~tmp来标识它); (2)执行vector的 ‘=’ 函数,释放对象v中的成员,并将~tmp的成员移动到v中,此时v中的成员就被替换成了~tmp中的成员; (3)删除临时对象~tmp。

关键的过程就是第2步,它不是复制而是移动,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。但是,这份免费的午餐也不是无条件就可以获取的,需要带上-std=c++11来编译。

2.右值引用

为了支持移动操作,C++11引入了一种新的引用类型——右值引用(rvalue reference)。所谓的右值引用指的是必须绑定到右值的引用。使用&&来获取右值引用。这里给右值下个定义:只能出现在赋值运算符右边的表达式才是右值。相应的,能够出现在赋值运算符左边的表达式就是左值,注意,左值也可以出现在赋值运算符的右边。对于常规引用,为了与右值引用区别开来,我们可以称之为左值引用(lvalue reference)。下面是左值引用与右值引用示例:

代码语言:javascript
复制
int i=42;
int& r=i;           //正确,左值引用
int&& rr=i;         //错误,不能将右值引用绑定到一个左值上
int& r2=i*42;       //错误,i*42是一个右值
const int& r3=i*42; //正确:可以将一个const的引用绑定到一个右值上
int&& rr2=i*42;     //正确:将rr2绑定到乘法结果上  

从上面可以看到左值与右值的区别有: (1)左值一般是可寻址的变量,右值一般是不可寻址的字面常量或者是在表达式求值过程中创建的可寻址的无名临时对象; (2)左值具有持久性,右值具有短暂性。

不可寻址的字面常量一般会事先生成一个无名临时对象,再对其建立右值引用。所以右值引用一般绑定到无名临时对象,无名临时对象具有如下两个特性: (1)临时对象将要被销毁; (2)临时对象无其他用户。 这两个特性意味着,使用右值引用的代码可以自由地接管所引用的对象的资源。关于无名临时对象,请参见认识C++中的临时对象temporary object

左值到右值引用的转换: 虽然不能直接将右值引用直接,但是我们可以显示地将一个左值转换为对应的右值引用类型。我们可以通过调用新标准库中的模板函数move来获得绑定到左值的右值引用。示例如下:

代码语言:javascript
复制
int&& rr1=42;
int&& rr2=rr1;              //error,表达式rr1是左值
int&& rr2=std::move(rr1);   //ok

上面的代码说明了右值引用也是左值,不能对右值引用建立右值引用。move告诉编译器,在对一个左值建立右值引用后,除了对左值进行销毁和重新赋值,不能够再访问它。std::move的VC10.0版本的STL库定义如下:

代码语言:javascript
复制
/*
 *  @brief  Convert a value to an rvalue.
 *  @param  __t  A thing of arbitrary type.
 *  @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept{
    return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); 
}

template<class _Ty> struct remove_reference{ 
   // remove reference
    typedef _Ty type;
};

template<class _Ty> struct remove_reference<_Ty&>{    
    // remove reference
    typedef _Ty type;
};

template<class _Ty> struct remove_reference<_Ty&&>{        
    // remove rvalue reference
    typedef _Ty type;
};

move的参数是接收一个任意类型的右值引用,通过引用折叠,此参数可以与任意类型实参匹配。特别的,我们既可以传递左值,也可以传递右值给move:

代码语言:javascript
复制
string s1("hi");
string&& s2=std::move(string("bye"));   //正确:从一个右值移动数据  
string&& s3=std::move(s1);              //正确:在赋值之后,s1的值是不确定的

关于引用折叠: 引用折叠指的是左值引用与右值引用相互赋值时会发生类型的变化,变化规则为: (1)所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& =&&变成A&&); (2)所有的其他引用类型之间的折叠都将变成左值引用。 (A&&=&变成 A&; A&=&&变成 A&)。

关于typename为什么会出现在std::move返回值前面的说明。 这里需要明白typename的两个作用,一个是申明模板中的类型参数,二是在模板中标明“内嵌依赖类型名”(nested dependent type name)[3]^{[3]}。

“内嵌依赖类型名”中“内嵌”是指类型定义在类中。以上type是定义在struct remove_reference;“依赖”是指依赖于一个模板参数,上面的std::remove_reference<_Tp>::type&&依赖模板参数_Tp。 “类型名”是指这里最终要指出的是个类型名,而不是变量。

3.右值引用的作用——实现移动构造函数和移动赋值运算符

右值引用的作用是用于移动构造函数(Move Constructors)和移动赋值运算符( Move Assignment Operator)。为了让我们自己定义的类型支持移动操作,我们需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,即拷贝构造和赋值运算符,但它们从给定对象窃取资源而不是拷贝资源。

移动构造函数: 移动构造函数类似于拷贝构造函数,第一个参数是该类类型的一个右值引用,同拷贝构造函数一样,任何额外的参数都必须有默认实参。完成资源移动后,原对象不再保留资源,但移动构造函数还必须确保原对象处于可销毁的状态。

移动构造函数的相对于拷贝构造函数的优点:移动构造函数不会因拷贝资源而分配内存,仅仅接管源对象的资源,提高了效率。

移动赋值运算符: 移动赋值运算符类似于赋值运算符,进行的是资源的移动操作而不是拷贝操作从而提高了程序的性能,其接收的参数也是一个类对象的右值引用。移动赋值运算符必须正确处理自赋值。

下面给出移动构造函数和移动析构函数利用右值引用来提升程序效率的实例,首先我先写了一个山寨的vector:

代码语言:javascript
复制
#include <iostream>
#include <string>
using namespace std;

class Obj{
public:
    Obj(){cout <<"create obj" << endl;}
    Obj(const Obj& other){cout<<"copy create obj"<<endl;}
};


template <class T> class Container{
public:
    T* value;
public:
    Container() : value(NULL) {};
    ~Container() {
        if(value) delete value; 
    }

    //拷贝构造函数
    Container(const Container& other){
        value = new T(*other.value);
        cout<<"in constructor"<<endl;
    }
    //移动构造函数
    Container(Container&& other){
        if(value!=other.value){
            value = other.value;
            other.value = NULL;
        }
        cout<<"in move constructor"<<endl;
    }
    //赋值运算符
    const Container& operator = (const Container& rhs){
        if(value!=rhs.value){
            delete value;
            value = new T(*rhs.value);
        }
        cout<<"in assignment operator"<<endl;
        return *this;
    }
    //移动赋值运算符
    const Container& operator = ( Container&& rhs){
        if(value!=rhs.value){
            delete value;
            value=rhs.value;
            rhs.value=NULL;
        }
        cout<<"in move assignment operator"<<endl;
        return *this;
    }

    void push_back(const T& item){
        delete value;
        value = new T(item);
    }
};

Container<Obj> foo(){
     Container<Obj> c;
     c.push_back(Obj());
     cout << "---- exit foo ----" << endl;
     return c;
}

int main(){
    Container<Obj> v;
    v=foo();    //采用移动构造函数来构造临时对象,再将临时对象采用移动赋值运算符移交给v
    getchar();
}

程序输出:

代码语言:javascript
复制
create obj
copy create obj
---- exit foo ----
in move constructor
in move assignment operator

上面构造的容器只能存放一个元素,但是不妨碍演示。从函数foo中返回容器对象全程采用移动构造函数和移动赋值运算符,所以没有出现元素的拷贝情况,提高了程序效率。如果去掉Container的移动构造函数和移动赋值运算符,程序结果如下:

代码语言:javascript
复制
create obj
copy create obj
---- exit foo ----
copy create obj
in constructor
copy create obj
in assignment operator

可见在构造容器Container的临时对象~tmp时发生了元素的拷贝,然后由临时对象~tmp再赋值给v时,又发生了一次元素的拷贝,结果出现了无谓的两次元素拷贝,这严重降低了程序的性能。由此可见,右值引用通过移动构造函数和移动赋值运算符来实现对象移动在C++程序开发中的重要性。

同理,如果想以左值来调用移动构造函数构造容器Container的话,那么需要将左值对象通过std::move来获取对其的右值引用,参考如下代码:

代码语言:javascript
复制
//紧接上面的main函数中的内容
    Container<Obj> c=v;             //调用普通拷贝构造函数,发生元素拷贝
    cout<<"-------------------"<<endl;
    Container<Obj> c1=std::move(v); //获取对v的右值引用,然后调用移动构造函数构造c
    cout<<c1.value<<endl;
    cout<<v.value<<endl;    //v的元素值已经在动构造函数中被置空(被移除)

代码输出:

代码语言:javascript
复制
copy create obj
in constructor
-------------------
in move constructor
00109598
00000000

参考文献

[1]Stanley B. Lippman著,王刚 杨巨峰译.C++ Primer中文版第五版.2013:470-485 [2]C++ 11 中的右值引用 [3]C++中typename关键字的使用方法和注意事项

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年09月08日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.对象移动
  • 2.右值引用
  • 3.右值引用的作用——实现移动构造函数和移动赋值运算符
  • 参考文献
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档