【1】std::move
在C++11中,标准库在
这个函数的名字很具有迷惑性,因为实际上std::move并不能移动任何东西,它唯一的功能:将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。
从实现上讲,std::move基本等同于一个类型转换:
static_cast(lvalue);
【2】应用注意项
(1)被std::move转化的左值,其生命期并没有随着转化而改变。
请看这个典型误用std::move的示例:
1 #include2 using namespace std; 3 4 class Moveable 5 { 6 public: 7 Moveable(): i(new int(3)) {} 8 ~Moveable() { delete i; } 9 Moveable(const Moveable & m): i(new int(*m.i)) { } 10 Moveable(Moveable&& m) : i(m.i) 11 { 12 m.i = nullptr; 13 } 14 15 int* i; 16 }; 17 18 int main() 19 { 20 Moveable a; 21 Moveable c(move(a)); // 会调用移动构造函数 22 cout << *a.i << endl; // 运行时错误 23 }
显然,为类型Moveable定义了移动构造函数。
这个函数定义本身没有什么问题,但调用的时候,使用了Moveable c(move(a));这样的语句。
这里的a本来是一个左值变量,通过std::move将其转换为右值。
这样一来,a.i就被c的移动构造函数设置为指针空值。
由于a的生命期实际要到main函数结束才结束,那么随后对表达式*a.i进行计算的时候,就会发生严重的运行时错误。
当然,标准库提供该函数的目的不是为了让程序员搬起石头砸自己的脚。
事实上,要使用该函数,必须是程序员清楚需要转换的时候。
比如上例中,程序员应该知道被转化为右值的a不可以再使用。
(2)通常情况下,需要转换成为右值引用的还确实是一个生命期即将结束的对象。
比如下例:
1 #include2 using namespace std; 3 4 class HugeMem 5 { 6 public: 7 HugeMem(int size) : sz(size > 0 ? size : 1) 8 { 9 c = new int[sz]; 10 } 11 ~HugeMem() 12 { 13 delete []c; 14 } 15 HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c) 16 { 17 hm.c = nullptr; 18 } 19 20 int* c; 21 int sz; 22 }; 23 24 class Moveable 25 { 26 public: 27 Moveable() : i(new int(3)), h(1024) { } 28 ~Moveable() { delete i; } 29 Moveable(Moveable && m) : i(m.i), h(move(m.h)) // 强制转为右值,以调用移动构造函数 30 { 31 m.i = nullptr; 32 } 33 34 int* i; 35 HugeMem h; 36 }; 37 38 Moveable GetTemp() 39 { 40 Moveable tmp = Moveable(); 41 cout << hex << "Huge Mem from " << __func__ << " @" << tmp. h. c << endl; // Huge Mem from GetTemp @0x0086E490 42 return tmp; 43 } 44 45 int main() 46 { 47 Moveable a(GetTemp()); 48 cout << hex << "Huge Mem from " << __func__ << " @" << a. h. c << endl; // Huge Mem from main @0x0086E490 49 }
定义了两个类型:HugeMem和Moveable,其中Moveable包含了一个HugeMem的对象。
在Moveable的移动构造函数中,我们就看到了std::move函数的使用。
该函数将m.h强制转化为右值,以迫使Moveable中的h能够实现移动构造。
这里可以使用std::move,是因为m.h是m的成员,既然m将在表达式结束后被析构,其成员也自然会被析构,因此不存在生存期不合理的问题。
关于std::move使用的必要性问题,在这里再赘述(可参见随笔《移动语义》)一遍:
如果不使用std::move(m.h)这样的表达式,而是直接使用m.h这个表达式,由于m.h是个左值,就会导致调用HugeMem的拷贝构造函数来构造Moveable的成员h。
如果是这样,移动语义就没有能够成功地向类的成员传递。换言之,还是会由于拷贝而导致一定的性能上的损失。
(3)如何判断一个类型是否具有可移动构造函数?
在标准库的头文件
is_move_constructible、
is_trivially_move_constructible、
is_nothrow_move_constructible,使用方法仍然是使用其成员value。示例代码:
1 #include2 #include 3 using namespace std; 4 5 class HugeMem 6 { 7 public: 8 HugeMem(int size) : sz(size > 0 ? size : 1) 9 { 10 c = new int[sz]; 11 } 12 ~HugeMem() 13 { 14 delete [] c; 15 } 16 HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c) 17 { 18 hm.c = nullptr; 19 } 20 21 int* c; 22 int sz; 23 }; 24 25 class Moveable 26 { 27 public: 28 Moveable() : i(new int(3)), h(1024) { } 29 ~Moveable() { delete i; } 30 Moveable(Moveable && m) noexcept : i(m.i), h(move(m.h)) // 强制转为右值,以调用移动构造函数 31 { 32 m.i = nullptr; 33 } 34 35 int* i; 36 HugeMem h; 37 }; 38 39 int main() 40 { 41 cout << is_move_constructible ::value << endl; // 1 测试类型是否具有移动构造函数 42 cout << is_move_constructible ::value << endl; // 1 43 cout << is_trivially_move_constructible ::value << endl; // 0 测试类型是否具有普通移动构造函数 44 cout << is_trivially_move_constructible ::value << endl; // 0 45 cout << is_nothrow_move_constructible ::value << endl; // 0 测试类型是否具有nothrow移动构造函数 46 cout << is_nothrow_move_constructible ::value << endl; // 1 47 }
可以判断是否具有移动构造函数、是否具有普通移动构造函数、是否具有不抛异常的移动构造函数。
(4)移动语义对泛型编程的积极意义
一个比较典型的应用是可以实现高性能的置换(swap)函数。
如下代码:
template <class T> void swap(T& a, T& b) { T tmp(move(a)); a = move(b); b = move(tmp); }
如果T是可以移动的,那么移动构造和移动赋值将会被用于这个置换。
代码中,a先将自己的资源交给tmp,随后b再将资源交给a,tmp随后又将从a中得到的资源交给b,从而完成了一个置换动作。
整个过程,代码都只会按照移动语义进行指针交换,不会有资源的释放与申请。
而如果T不可移动却是可拷贝的,那么拷贝语义会被用来进行置换。这就跟普通的置换语句是相同的了。
因此在移动语义的支持下,我们仅仅通过一个通用的模板,就可能更高效地完成置换。
综上所述:
实际上,为了保证移动语义的传递,程序员在编写移动构造函数的时候,应该总是记得使用std::move转换拥有形如堆内存、文件句柄等资源的成员为右值。
这样一来,如果成员支持移动构造的话,就可以实现其移动语义。
而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。
good good study, day day up.
顺序 选择 循环 总结