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

来源:互联网 发布:微游戏机 vs 网络盒子 编辑:程序博客网 时间:2024/05/29 18:58

一.前言

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

  • 临时对象的产生
  • 何谓右值引用
  • 何谓移动语义


二.临时对象的产生

在我们以往的编程过程中可能很少会注意到临时对象(变量)的问题,因为这已经不太是程序层面上的问题,而更多是编译器上的事情。编译器在编译代码过程中为了实现某些代码可能会产生出一些临时对象(变量)来满足一些效果,比如:

void Test(MyClass obj) {}MyClass myclass;Test(myclass);//可能编译器会做如下改写:void Test(MyClass& obj){}MyClass myclass;MyClass obj(myclass);    //产生的临时对象Test(obj);obj.~MyClass();

又或者是这样:

double db = 5.5;int it = st;      //产生int临时变量赋值给it

当然,这些临时对象并不需要程序员参与干涉,甚至临时对象这些行为对于程序员而言是透明的。但是,我们仍然需要对其进行一定的了解,这对后面c++11引入的右值引用和移动语义的理解会带来莫大的帮助。

产生临时变量的地方有很多,不同编译器在某些细节上可能处理方式也略有不同。不过具体说来有以下几种:

  • 不同类型(对象)变量的转换
  • 函数以pass by value传递参数的时候
  • 表达式求值中

这里文章就不继续深入探讨了,下面我们来谈临时对象对编程的一些影响和右值引用的引入。


三.何谓右值引用

假如我们设计一个复数类,可能大概如下:

class Complex{public:    Complex() :_real(0), _imaginary(0) {}    Complex(double real, double imaginary) :_real(real), _imaginary(imaginary) {}    Complex(Complex& cp) :_real(cp._real), _imaginary(cp._imaginary) {}private:    double _real;             //实部    double _imaginary;        //虚部};

嗯,先不看其他函数,就此而言大概是”没有太多问题的”。
但是如果在如下使用时候:

//求共轭复数//为Complex的友元Complex getConjugate(Complex cp){    cp._imaginary = -cp._imaginary;    return cp;}int main(){    Complex a(1.0, -5.0);    Complex b = getConjugate(a);  // Error : no match    return 0;}

有的编译器下会报错。MSVC 没有, GUN GCC 报错了。原因呢很简单,编译器提示找不到匹配的拷贝构造函数。
我们在前面的介绍知道,这里函数返回的其实是一个临时对象,而我们拷贝构造函数中的参数类型为Complex&。而这里,试图将一个引用绑定到临时变量上,这显然是不允许的。因为,规定不允许改变临时变量。
所以我们也知道了解决方案:将拷贝构造函数修改为 Complex(const Complex& cp)

以上问题就告一段落了,我们来到一个新问题,我们试图写一个mystring类:

class MyString{    friend ostream& operator << (ostream& os, MyString mystr);public:    MyString() :_ptr(0) {}    MyString(const char* ptr) {        int len = strlen(ptr);        _ptr = new char[len + 1];        strcpy(_ptr, ptr);    }    MyString(const MyString& mystr) {        int len = strlen(mystr._ptr);        _ptr = new char[len + 1];        strcpy(_ptr, mystr._ptr);    }    //MyString& operator = (const MyString&){ ... }    ~MyString() { delete _ptr; }private:    char* _ptr;};ostream& operator << (ostream& os, MyString mystr){    return os << mystr._ptr;}int main(){    MyString a = "123";    cout << a << endl;    return 0;}

由于篇幅,我就只是给出了必要的函数,operator=()就没有给出了。所以赋值只是浅拷贝,不过这里并不关注这个。

我们注意看拷贝构造函数,由于前篇提到的原因,这里的拷贝函数我们加上了const,使得拷贝函数能够接受临时变量。
当传入参数是临时变量的情况时,我们仔细思考一下:
我们知道临时变量很快(大概就是函数返回之后)就会销毁,然而在这样的情况下,行为仿佛是:将一份字符串拷贝一份,然后就将原稿销毁了。对,为什么不能将临时对象的字符串”拿为己用”呢?这样我们就免去了内存的开辟和繁琐的字符串赋值了。嗯,大概函数如下:

MyString(....):_ptr(mystr._ptr){    mystr._ptr = NULL;}

对,这样我们就将_ptr指向的真正的字符串的所有权拿过来了。但是,这里我把参数空了出来,是的,我们要去设法判断什么时候是临时对象。然而这在c++1.0是不太可行的。

所以,在C++11中引入了一个新的概念——右值引用(&&),右值引用专门用于引用右值(临时对象、匿名对象)。

那么有了语言的支持,对于上面的情况,我们可以进行如下的改写:

class MyString{    friend ostream& operator << (ostream& os, MyString mystr);public:    //上面不变...     MyString(const MyString& mystr) {        int len = strlen(mystr._ptr);        _ptr = new char[len + 1];        strcpy(_ptr, mystr._ptr);    }    MyString(MyString&& mystr) :_ptr(mystr._ptr){        mystr._ptr = NULL;    }    //下面不变...};

这样的话,我们当传入临时变量的时候,哦,不对,我们应该改口叫做右值。
那么调用的编译器就是MyString(MyString&& mystr)版本,在函数中,我们剥夺了右值的字符串,并且将mystr._ptr = NULL, 这很重要,因为防止临时变量析构的时候销毁字符串。


四.何谓移动语义

其实上一段在介绍mystring设计的时候已经使用了移动语义,就是程序员提供移动构造函数(参数为右值引用的构造函数)使得当用临时变量构造新对象的时候可以提供较好的优化。
下面给就用上面的mystring作为例子来测一下提供了移动构造函数和没有提供移动构造函数的时候,分别用临时变量去构造对象的对比:

  100000长度字符串,10000次初始化 提供移动构造 22ms 不提供移动构造 376ms

可以看出来提供移动构造函数在某些情况下可以将性能极大地提高。特别对于某些需要深拷贝的对象来说。特别的,当这一类的对象配合标准库的容器的时候(vector,deque),如果提供移动构造函数,将会在容器内存重分配的时候带来极大效率的优化。

当然,除了编译器创建的临时对象来作为右值以外,我们也可以使用std::move来得到一个左值的右值引用。
我们先来看move()的源码:

template<class _Ty> inlineconstexpr typename remove_reference<_Ty>::type&&move(_Ty&& _Arg) _NOEXCEPT{    return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));}

当传入左值Type&的时候 _Ty被识别为Type& , remove_reference<_Ty>::type 为 Type,返回的就是 Type&&
当传入右值Type&&的时候_Ty被识别为Type&&, remove_reference<_Ty>::type 为 Type,返回的就是 Type&&

也就是无论传入一个什么值,都将返回这个值的右值引用,这样就可以显式让编译器调用类中的移动构造函数了。不过值得注意的是,程序员必须恪守——被当作右值的对象在移动构造之后在重新赋值之前绝不使用。
如下:

vector<int> a = {1,2,3,4,5};vector<int> b(move(a));       //这里调用的是b的移动构造函数//a在重新赋值之前绝不使用。
2 0
原创粉丝点击