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

来源:互联网 发布:清真食品卫生吗 知乎 编辑:程序博客网 时间:2024/06/05 12:09

1.对象移动

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

//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();}

编译并运行:

[b3335@localhost test]$ g++ moveobj.cpp[b3335@localhost test]$ ./a.out create objcopy create obj---- exit foo ----copy create obj

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

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

[b3335@localhost test]$ g++ -std=c++11 moveobj.cpp [b3335@localhost test]$ ./a.out create objcopy 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)。下面是左值引用与右值引用示例:

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来获得绑定到左值的右值引用。示例如下:

int&& rr1=42;int&& rr2=rr1;              //error,表达式rr1是左值int&& rr2=std::move(rr1);   //ok

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

/* *  @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:

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]

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

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

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

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

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

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

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

#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();}

程序输出:

create objcopy create obj---- exit foo ----in move constructorin move assignment operator

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

create objcopy create obj---- exit foo ----copy create objin constructorcopy create objin assignment operator

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

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

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

代码输出:

copy create objin constructor-------------------in move constructor0010959800000000

参考文献

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

1 0
原创粉丝点击