概述
本篇博客根据深入理解C++11新特性解析与应用
一书中的内容以及自己在使用std::move
过程中的经验,总结形成该篇博客,将它整理形成知识。该书我已经高清书签版上传到CSDN,为了防止不过,修改了文件名称,下载地址奉上。
深入理解C++11新特性解析与应用
关键字std::move
概述
在C++11中,标准库在<utility>
中提供了一个有用的函数std::move
,这个函数的名字具有迷惑性,因为实际上std::move并不能够移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。
下面是std::move
可能的实现的一种方式。
template< class T >
typename std::remove_reference<T>::type&& move(T&& t) noexcept
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
从上诉代码中,可以看出std::move
基本等同于一个类型转换:
statc_cast<T&&>(lvalue);
错误用例
需要注意的是被转化的值,其生命期并没有随着左右值的转换而改变,即被std::move转化的左值变量lvalue
并不会被立即析构。
#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; // 运行时错误
}
上述代码中a
本来是一个左值变量,通过std::move
将其转换为右值。这样一来,a.i
就被c
的移动构造函数设置为指针空值。由于a
的生命周期要到main
函数结束才结束,因此对表达式*a.i
进行计算的时候,就会发生严重的运行时错误。
上述代码是典型误用std::move
的例子。
正确用例1
要正确使用该函数,必须是程序员清楚需要转换的时候,比如上例中,程序员应该知道被转换为右值的a
不可以再使用。不过更多地,我们需要转换成为右值引用的还是一个确实声明期即将结束的对象。下面给出正确的用例。
#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
}
上述例子我们定义了两个类型:HugeMem
和Moveable
,其中Moveable
包含了一个HugeMem
的对象。在Moveable
的移动构造函数中,我们使用了std::move
,它将m.h
强制转换为右值,以使用Moveable
中的h
能够实现移动构造,由于GetTemp()
返回的是右值,因此m
将在表达式结束后被析构,其成员自然也被析构。
正确用例2
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
// uses the push_back(const T&) overload, which means
// we'll incur the cost of copying str
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
// uses the rvalue reference push_back(T&&) overload,
// which means no strings will be copied; instead, the contents
// of str will be moved into the vector. This is less
// expensive, but also means str might now be empty.
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
总结
事实上,为了保证移动语义的传递,程序员在编写移动构造函数的时候,应该总是记得使用std::move
转换拥有的形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。