在C++11中,标准库在<utility>
中提供了一个有用的函数std::move
,这个函数的名字具有迷惑性。
实际上std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。
从实现上讲,std::move基本等同于一个类型转换:
static_cast<T&&>(lvalue);
值得一提的是,被转化的左值,其生命期并没有随着左右值的转化而改变。
如果读者期望std::move转化的左值变量lvalue能立即被析构,那么肯定会失望了。
我们来看代码清单3-21所示的例子。
代码清单3-21
#include <iostream>
using namespace std;
class Moveable
{
public:
int* i;
Moveable() : i(new int(3))
{}
~Moveable(){
delete i;
}
Moveable(const Moveable& m)
: i(new int(*m.i)){
}
Moveable(Moveable&& m)
: i(m.i){
m.i = nullptr;
}
};
int main()
{
Moveable a;
Moveable c(move(a)); //会调用移动构造函数
cout << *a.i << endl;//运行时错误
}
// 编译选项:g++ -std=c++11 3-3-6.cpp -fno-elide-constructors
在代码清单3-21中,我们为类型Moveable定义了移动构造函数。
这个函数定义本身没有什么问题,但调用的时候,使用了Moveable c(move(a));这样的语句。这里的a本来是一个左值变量,通过std::move将其转换为右值。这样一来,a.i就被c的移动构造函数设置为指针空值。由于a的生命期实际要到main函数结束才结束,那么随后对表达式*a.i进行计算的时候,就会发生严重的运行时错误。
这是个典型误用std::move的例子。当然,标准库提供该函数的目的不是为了让程序员搬起石头砸自己的脚。事实上,要使用该函数,必须是程序员清楚需要转换的时候。比如上例中,程序员应该知道被转化为右值的a不可以再使用。
不过更多地,我们需要转换成为右值引用的还是一个确实生命期即将结束的对象。
我们来看看代码清单3-22所示的正确例子。
代码清单3-22
#include <iostream>
using namespace std;
class HugeMem
{
public:
int sz;
int* c;
HugeMem(int size)
: sz(size > 0 ? size : 1){
c = new int[sz];
}
~HugeMem(){
delete[] c;
}
HugeMem(HugeMem&& hm)
: sz(hm.sz),c(hm.c){
hm.c = nullptr;
}
};
class Moveable{
public:
int* i;
HugeMem h;
Moveable()
:i(new int(3)), h(1024)
{}
~Moveable() {
delete i;
}
Moveable(Moveable && m)
:i(m.i), h(move(m.h)) { // 强制转为右值,以调用移动构造函数
m.i = nullptr;
}
};
Moveable GetTemp() {
Moveable tmp = Moveable();
cout << hex << "Huge Mem from " << "GEtTemp"
<< " @" << tmp.h.c << endl; // Huge Mem from GEtTemp @01059D88
return tmp;
}
int main() {
Moveable a(GetTemp());
cout << hex << "Huge Mem from " << "main"
<< " @" << a.h.c << endl; // Huge Mem from main @01059D88
}
// 编译选项:g++ -std=c++11 3-3-7.cpp -fno-elide-constructors
在代码清单3-22中,我们定义了两个类型:HugeMem和Moveable,其中Moveable包含了一个HugeMem的对象。
在Moveable的移动构造函数中,我们就看到了std::move函数的使用。
该函数将m.h强制转化为右值,以迫使Moveable中的h能够实现移动构造。这里可以使用std::move,是因为m.h是m的成员,既然m将在表达式结束后被析构,其成员也自然会被析构,因此不存在代码清单3-21中的生存期不对的问题。另外一个问题可能是std::move使用的必要性。这里如果不使用std::move(m.h)这样的表达式,而是直接使用m.h这个表达式将会怎样?
其实这是C++11中有趣的地方:可以接受右值的右值引用本身却是个左值。
这里的m.h引用了一个确定的对象,而且m.h也有名字,可以使用&m.h取到地址,因此是个不折不扣的左值。
不过这个左值确确实实会很快“灰飞烟灭”,因为拷贝构造函数在Moveable对象a的构造完成后也就结束了。那么这里使用std::move强制其为右值就不会有问题了。而且,如果我们不这么做,由于m.h是个左值,就会导致调用HugeMem的拷贝构造函数来构造Moveable的成员h(虽然这里没有声明,读者可以自行添加实验一下)。如果是这样,移动语义就没有能够成功地向类的成员传递。换言之,还是会由于拷贝而导致一定的性能上的损失。
事实上,为了保证移动语义的传递,程序员在编写移动构造函数的时候,应该总是记得使用std::move转换拥有形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。
《深入理解C++11》