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
这是我们在以前的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 对你来说,应该已经够用了。