[C++11] Move Semantics

1. lvalue和rvalue

一切都要从两个古老的概念开始讲起:lvalue和rvalue。先看一段代码:

int a = 3;
6 = a;

这段代码一看就是有问题的,而且显然问题出在了第二行。那么问题到底是什么呢?答案就是:6是一个rvalue,我们不能把rvalue放在赋值操作的左边。这样看来,lvalue和rvalue的意思就呼之欲出了:lvalue是可以放在赋值操作左边的;而rvalue不行。(虽然这不是严格的定义,而且也有反例。但这明显有助于我们理解概念。)

一些常见的有关rvalue的错误如下:

(var + 1) = 4; // ERROR!
加运算符返回的值是一个rvalue,所以上面这句话是错的。再比如:

int foo() {return 2;}

int main()
{
    foo() = 2;

    return 0;
}

被foo()返回的的是一个rvalue,也不能被赋值。

lvalue和rvalue两者之间一个最显著的区别在于:lvalue可以被modified;而rvalue不行。一个(正确的)直觉是:rvalue是一个临时的变量,用完了就消失于无形之中。

有人说lvalue和rvalue的区别是:lvalue在内存上有地址;而rvalue在内存上没有地址。这句话不完全正确。rvalue在内存上也有地址,只不过是用完之后,这块地址立刻就被回收了。这就是为什么说它是临时的。

有关lvalue和rvalue还有其他一些有趣的话题,比如他们之间的转换等,这里不做赘述。但大部分的规定都是intuitive的,我相信读者已经能够领会到lvalue和rvalue的区别了。


2. lvalue reference,rvalue reference和move constructor

lvalue reference就是以前的reference,它自古有之,我们见得也很多。一个经典的例子:

void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b = temp;
}

int main() {
	int x = 3;
	int y = 6;
	swap(x, y);
	cout << x << ", " << y << endl;
	return 0;
}

输出的结果是6, 3。这是一个pass by reference的例子,不多说。

rvalue reference是C++11新有的特性,它是为move semantics而存在的。先来看下面的代码:

int two () {
	return 2;
}

int main() {
	const int &a = two(); 	// ok!
	int &b = two(); 		// error!
}
在main函数中,第一行没有问题。虽然two这个函数返回的是一个rvalue,但需要记住的是,rvalue可以保存在一个const lvalue reference中。这背后的原理可以这样理解:rvalue的一个重要特征就是不能改变值;而const lvalue reference也不能改变值,所以它可以保存rvalue。

但是第二行就有问题了,一个非const的lvalue reference是不能保存rvalue的。原因很简单,假设可以保存,那么我们就可以改变b的值,这样也就改变了rvalue的值,那肯定是不行的。但现在我们有了rvalue reference,世界变得不一样了:

int two () {
	return 2;
}

int main() {
	const int &a = two(); 	// ok!
	int &&b = two(); 		// ok!
}
其实呢,背后的原理也没有多复杂:原来我们只能用const lvalue reference来保存rvalue,这意味着我们不能改变它;而现在我们可以改变它了,只不过需要用的是rvalue reference。就这么简单。说得更详细一点:在main函数的第二行,two返回了一个rvalue。正如我们刚才所说,它其实也是有一个地址的。在之前,我们认为这个地址是临时的,用过之后马上就应该消失,所以就算我们保存它(用const lvalue reference),我们也不应该改变它。而现在的情况是,我们不但可以保存它,我们也可以改变它,只需要使用rvalue reference即可。

好了,一个问题产生了:它有神马用呢?

答案:move semantics!来看下面这段代码:

#include <iostream>
using namespace std;

class ArrayWrapper
{
    public:
        // default constructor produces a moderately sized array
        ArrayWrapper ()
            : _p_vals( new int[ 64 ] )
            , _size( 64 )
        {
			cout << "Default constructor: " << this << endl;
		}
     
        explicit ArrayWrapper (int n)
            : _p_vals( new int[ n ] )
            , _size( n )
        {
			cout << "Constructor: " << this << endl;
		}

        // copy constructor
        ArrayWrapper (const ArrayWrapper& other)
            : _p_vals( new int[ other._size  ] )
            , _size( other._size )
        {
				cout << "Copy constructor: " << this << endl;
                for ( int i = 0; i < _size; ++i )
                {
                        _p_vals[ i ] = other._p_vals[ i ];
                }
        }
        ~ArrayWrapper ()
        {
				cout << "Destructor: " << this << endl;
                delete [] _p_vals;
        }
     
    public:
        int *_p_vals;
        int _size;
};

ArrayWrapper two() {
	ArrayWrapper a(7);
	cout << "Temp ArrayWrapper created!" << endl;
	return a;
}

int main() {
	ArrayWrapper b (two());
}

这段代码在我的机器上的输出是:

Constructor: 0x7fff5b6f1b80
Temp ArrayWrapper created!
Destructor: 0x7fff5b6f1b80
Copy constructor: 0x7fff5b6f1be0
Destructor: 0x7fff5b6f1bd0
Destructor: 0x7fff5b6f1be0


好,我们可以看到,第一行输出是在two()函数中,构建a这个local variable时候所调用的constructor;第三行输出是two()函数结束时候,a也随之消亡,于是destructor被调用。有趣的地方出现在输出的第四行:copy constructor被调用了。这是很容易理解的:我们在main函数里,构建了一个b,并且是用一个rvalue来构建的。那么很显然copy constructor会被调用,因为它的参数是一个const lvalue reference, 那么刚刚我们说过了,一个const lvalue reference可以用来保存rvalue。注意:如果把copy constructor的参数中的const去掉,那么这段程序编译不通过。

这是我们在以前的C++程序中会经常见到的一种处理方法。那么我们现在来看,使用C++11的新feature会有什么不同以及有什么好处。我们加上move constructor。

#include <iostream>
using namespace std;

class ArrayWrapper
{
    public:
        // default constructor produces a moderately sized array
        ArrayWrapper ()
            : _p_vals( new int[ 64 ] )
            , _size( 64 )
        {
            cout << "Default constructor: " << this << endl;
        }
     
        explicit ArrayWrapper (int n)
            : _p_vals( new int[ n ] )
            , _size( n )
        {
            cout << "Constructor: " << this << endl;
        }

        // move constructor
        ArrayWrapper (ArrayWrapper&& other)
            : _p_vals( other._p_vals  )
            , _size( other._size )
        {
                cout << "Move constructor: " << this << endl;
                other._p_vals = NULL;
                other._size = 0;
        }

        // copy constructor
        ArrayWrapper (const ArrayWrapper& other)
            : _p_vals( new int[ other._size  ] )
            , _size( other._size )
        {
                cout << "Copy constructor: " << this << endl;
                for ( int i = 0; i < _size; ++i )
                {
                        _p_vals[ i ] = other._p_vals[ i ];
                }
        }
        ~ArrayWrapper ()
        {
                cout << "Destructor: " << this << endl;
                delete [] _p_vals;
        }
     
    public:
        int *_p_vals;
        int _size;
};

ArrayWrapper two() {
	ArrayWrapper a(7);
	cout << "Temp ArrayWrapper created!" << endl;
	return a;
}

int main() {
	ArrayWrapper b (two());
}


我们再一次编译并执行这段程序。

(注意,编译的时候需要加上-fno-elide-constructors的flag,不然的话编译器会进行copy elision。 这个问题困扰了我好几个小时,后来被StackOverflow上的大神瞬间解决了。详见这里。)

(再注意:居然因为研究得比较深入而发现了clang++中, -fno-elide-constructors flag的一个bug,暂且按下不表。)

在我机器上的输出是这样的:

Constructor: 0x7fff554f3b80
Temp ArrayWrapper created!
Destructor: 0x7fff554f3b80
Move constructor: 0x7fff554f3be0
Destructor: 0x7fff554f3bd0
Destructor: 0x7fff554f3be0

我们注意到,copy constructor没有了,取而代之的是move constructor。这给我们的启示是,我们现在有了一个方法来分辨lvalue和rvalue了:当一个rvalue做函数的参数时候,他会自动去匹配那个参数是rvalue reference的函数。(上面输出结果的倒数第二行就是我说的bug,可以先不用管)。

好,那接下来的问题就是,这样有什么好处呢?

我们看到,如果我们调用copy constructor(正如第一段代码那样),那么我们所做的事情是:开辟一块新的空间,把another上面的所有内容复制到新的空间里。但这里面存在一个可以优化的空间:被复制的值是一个rvalue,也就是说,复制完成之后,它就灰飞烟灭。但为什么一定要这样呢?为什么我我们要先复制一份,然后再把原有的毁掉呢?这不是浪费么?那么move constructor解决了这个问题:我们现在把rvalue里面的资源拿过来,为我所用,然后把rvalue的指针改成空指针,这样rvalue消失的时候就不会产生问题了。那么rvalue reference的价值就体现出来了:我们需要改变这个rvalue里面的指针。按照以往的习惯,rvalue怎么可能被改变呢?但是有了rvalue reference,一切变得可能了。

3. move assignment operator:

我们先来看看以前的assignment operator是怎么写的:

ArrayWrapper& operator= (const ArrayWrapper &another) {
	ArrayWrapper temp (another);
	swap(temp, *this);
}

(swap函数略过。)

好,如果我们的代码是这样的:

ArrayWrapper a(5);
ArrayWrapper b;
b = a;

那么,这个assignment operator没有任何问题。但如果我们的代码是这样的:

ArrayWrapper a;
a = two();

那么我们看到,assignment operator现在传进去的参数是一个rvalue。那么我们之前也说了,const lvalue reference可以保存一个rvalue,所以呢,这段代码也是可以编译运行的。但是问题在于:效率比较低!two()函数返回的rvaue座位参数传进了assignment operator之后,被复制了一次。那么assignment operator出来之后,作为rvalue它就消失了。同样的问题,我们为什么要这样呢?我们为什么要先复制一个东西,然后再让他消失呢?为什么不能直接把那个东西拿来用呢?于是乎,move assignment operator应运而生:

ArrayWrapper& operator= (ArrayWrapper another) {  // note the missing reference{
    std::swap(_p_vals, another._p_vals);
    return *this;
}

有人会问了:为什么你这里参数不用rvalue reference了呢?这还是move semantics么?Good question, 容我慢慢道来。

这里用了一个pass by value。按照一般的逻辑,这里应该会复制一个参数进来。但是少侠别忘了,我们现在又多了一个move constructor了!也就是说,如果我们调用的方法如下:

ArrayWrapper b = two();

那么参数another被传入了一个rvalue。那么因为它是一个rvalue,过去我们所使用的copy constructor在这里被换成了move constructor。于是乎,我们没有copy,而是把那个rvalue的内容给move了过来。同样实现了节省时间。而如果分析函数的内部,我们所做的事情是:1)swap两个;2)return。那么当这个函数return的时候,another作为一个local variable也就随之消失了。一个字:Elegant! 

有关C++11中的move semantics暂时讲到这里。@zz 对你来说,应该已经够用了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值