C++11 std::move 强制转换为右值

【1】std::move

在C++11中,标准库在<utility>中提供了一个有用的函数std::move。

这个函数的名字很具有迷惑性,因为实际上std::move并不能移动任何东西,它唯一的功能:将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。

从实现上讲,std::move基本等同于一个类型转换:

static_cast<T&&>(lvalue);

【2】应用注意项

(1)被std::move转化的左值,其生命期并没有随着转化而改变。

请看这个典型误用std::move的示例:

 1 #include <iostream>
 2 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 #include <iostream>
 2 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)如何判断一个类型是否具有可移动构造函数?

在标准库的头文件<type_traits>里,我们还可以通过一些辅助的模板类来判断一个类型是否是可以移动的。比如:

is_move_constructible、

is_trivially_move_constructible、

is_nothrow_move_constructible,使用方法仍然是使用其成员value。示例代码:

 1 #include <iostream>
 2 #include <type_traits>
 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<HugeMem>::value << endl;             // 1 测试类型是否具有移动构造函数
42     cout << is_move_constructible<Moveable>::value << endl;            // 1
43     cout << is_trivially_move_constructible<HugeMem>::value << endl;   // 0 测试类型是否具有普通移动构造函数
44     cout << is_trivially_move_constructible<Moveable>::value << endl;  // 0
45     cout << is_nothrow_move_constructible<HugeMem>::value << endl;     // 0 测试类型是否具有nothrow移动构造函数
46     cout << is_nothrow_move_constructible<Moveable>::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.

顺序 选择 循环 总结

posted @ 2020-01-27 00:18  kaizenly  阅读(1024)  评论(0编辑  收藏  举报
打赏