std::move:强制转化为右值

来源:互联网 发布:淘宝app抢购需要刷新吗 编辑:程序博客网 时间:2024/05/22 00:27
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:
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* i;
};

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:
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;
}
int * c;
int sz;
};
class Moveable{
public:
Moveable():i(new int(3)), h(1024) {}
~Moveable() { delete i; }
Moveable(Moveable && m):
i(m.i), h(move(m.h)) { // 强制转为右值,以调用移动构造函数
m.i = nullptr;
}
int* i;
HugeMem h;
};

Moveable GetTemp() {
Moveable tmp = Moveable();
cout << hex << "Huge Mem from " << __func__
<< " @" << tmp.h.c << endl; // Huge Mem from GetTemp @0x603030
return tmp;
}

int main() {
Moveable a(GetTemp());
cout << hex << "Huge Mem from " << __func__
<< " @" << a.h.c << endl; // Huge Mem from main @0x603030
}
// 编译选项: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转换拥有形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。
阅读全文
0 0