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

来源:互联网 发布:kingcms php 编辑:程序博客网 时间:2024/05/01 01:36

在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》

0 0
原创粉丝点击