移动构造函数和移动赋值

一、概述

移动构造函数可以弥补拷贝构造函数的空缺。

移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。

举个栗子。

问题一:如何将大象放入冰箱?
答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。

问题二:如何将大象从一台冰箱转移到另一台冰箱?
普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。
2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。
等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?

“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。

为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。

右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。

右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast<T&&>()的简单封装。

右值引用至少可以解决以下场景中的移动语义缺失问题:

  • 按值传入参数
按值传参是最符合人类思维的方式。基本的思路是,如果传入参数是为了将资源交给函数接受者,就应该按值传参。同时,按值传参可以兼容任何的cv-qualified左值、右值,是兼容性最好的方式。

其使用方式如下:

 1 class People {
 2 public:
 3   People(string name) // 按值传入字符串,可接收左值、右值。接收左值时为复制,接收右值时为移动
 4   : name_(move(name)) // 显式移动构造,将传入的字符串移入成员变量
 5   {
 6   }
 7   string name_;
 8 };
 9 
10 People a("Alice"); // 移动构造name
11 
12 string bn = "Bob";
13 People b(bn); // 拷贝构造name
14 

构造a时,调用了一次字符串的构造函数和一次字符串的移动构造函数。如果使用const string& name接收参数,那么会有一次构造函数和一次拷贝构造,以及一次non-trivial的析构。尽管看起来很蛋疼,尽管编译器还有优化,但从语义来说按值传入参数是最优的方式。

如果你要在构造函数中接收std::shared_ptr<X>并且存入类的成员(这是非常常见的),那么按值传入更是不二选择。拷贝std::shared_ptr<X>需要线程同步,相比之下移动std::shared_ptr是非常轻松愉快的。

  • 按值返回
和接收输入参数一样,返回值按值返回也是最符合人类思维的方式。曾经有无数函数为了返回容器而不得不写成这样
void str_split(const string& s, vector<string>* vec); // 一个按值语义定义的字符串拆分函数。这里不考虑分隔符,假定分隔符是固定的。

这样要求vec在外部被事先构造,此时尚无从得知vec的大小。即使函数内部有办法预测vec的大小,因为函数并不负责构造vec,很可能仍需要resize。
对这样的函数嵌套调用更是痛苦的事情,谁用谁知道啊。

有了移动语义,就可以写成这样

1 vector<string> str_split(const string& s) {
2   vector<string> v;
3   // ...
4   return v; // v是左值,但优先移动,不支持移动时仍可复制。
5 }

如果函数按值返回,return语句又直接返回了一个栈上的左值对象(输入参数除外)时,标准要求优先调用移动构造函数,如果不符再调用拷贝构造函数。尽管v是左值,仍然会优先采用移动语义,返回vector<string>从此变得云淡风轻。此外,无论移动或是拷贝,可能的情况下仍然适用编译器优化,但语义不受影响。

对于std::unique_ptr来说,这简直就是福音。
1 unique_ptr<SomeObj> create_obj(/*...*/) {
2   unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));
3   ptr->foo(); // 一些可能的初始化
4   return ptr;
5 }

当然还有更简单的形式

unique_ptr<SomeObj> create_obj(/*...*/) {
  return unique_ptr<SomeObj>(new SomeObj(/*...*/));
}

二、使用方法

基类:

 1 class MemoryBlock  
 2 {  
 3 public:  
 4   
 5    // Simple constructor that initializes the resource.  
 6    explicit MemoryBlock(size_t length)  
 7       : _length(length)  
 8       , _data(new int[length])  
 9    {  
10       std::cout << "In MemoryBlock(size_t). length = "  
11                 << _length << "." << std::endl;  
12    }  
13   
14    // Destructor.  
15    ~MemoryBlock()  
16    {  
17       std::cout << "In ~MemoryBlock(). length = "  
18                 << _length << ".";  
19   
20       if (_data != nullptr)  
21       {  
22          std::cout << " Deleting resource.";  
23          // Delete the resource.  
24          delete[] _data;  
25       }  
26   
27       std::cout << std::endl;  
28    }  
29   
30    // Copy constructor.  
31    MemoryBlock(const MemoryBlock& other)  
32       : _length(other._length)  
33       , _data(new int[other._length])  
34    {  
35       std::cout << "In MemoryBlock(const MemoryBlock&). length = "   
36                 << other._length << ". Copying resource." << std::endl;  
37   
38       std::copy(other._data, other._data + _length, _data);  
39    }  
40   
41    // Copy assignment operator.  
42    MemoryBlock& operator=(const MemoryBlock& other)  
43    {  
44       std::cout << "In operator=(const MemoryBlock&). length = "   
45                 << other._length << ". Copying resource." << std::endl;  
46   
47       if (this != &other)  
48       {  
49          // Free the existing resource.  
50          delete[] _data;  
51   
52          _length = other._length;  
53          _data = new int[_length];  
54          std::copy(other._data, other._data + _length, _data);  
55       }  
56       return *this;  
57    }  
58   
59    // Retrieves the length of the data resource.  
60    size_t Length() const  
61    {  
62       return _length;  
63    }  
64   
65 private:  
66    size_t _length; // The length of the resource.  
67    int* _data; // The resource.  
68 };  

移动构造函数:

  1. 定义一个空的构造函数方法,该方法采用一个对类类型的右值引用作为参数,如以下示例所示:
  2. 在移动构造函数中,将源对象中的类数据成员添加到要构造的对象:
  3. 将源对象的数据成员分配给默认值。 这可以防止析构函数多次释放资源(如内存):
     1 // Move constructor.  
     2 MemoryBlock(MemoryBlock&& other)  
     3    : _data(nullptr)  
     4    , _length(0)  
     5 {  
     6    std::cout << "In MemoryBlock(MemoryBlock&&). length = "   
     7              << other._length << ". Moving resource." << std::endl;  
     8   
     9    // Copy the data pointer and its length from the   
    10    // source object.  
    11    _data = other._data;  
    12    _length = other._length;  
    13   
    14    // Release the data pointer from the source object so that  
    15    // the destructor does not free the memory multiple times.  
    16    other._data = nullptr;  
    17    other._length = 0;  
    18 }  

     

移动赋值运算符:

1、定义一个空的赋值运算符,该运算符采用一个对类类型的右值引用作为参数并返回一个对类类型的引用,
2、在移动赋值运算符中,如果尝试将对象赋给自身,则添加不执行运算的条件语句。

3、在条件语句中,从要将其赋值的对象中释放所有资源(如内存)。以下示例从要将其赋值的对象中释放 _data 成员,执行第一个过程中的步骤 2 和步骤 3 以将数据成员从源对象转移到要构造的对象,

4、返回对当前对象的引用,如以下示例所示:

 1 MemoryBlock& operator=(MemoryBlock&& other)  
 2 {  
 3    std::cout << "In operator=(MemoryBlock&&). length = "   
 4              << other._length << "." << std::endl;  
 5   
 6    if (this != &other)  
 7    {  
 8       // Free the existing resource.  
 9       delete[] _data;  
10   
11       // Copy the data pointer and its length from the   
12       // source object.  
13       _data = other._data;  
14       _length = other._length;  
15   
16       // Release the data pointer from the source object so that  
17       // the destructor does not free the memory multiple times.  
18       other._data = nullptr;  
19       other._length = 0;  
20    }  
21    return *this;  
22 }  

 

三、意义及其与拷贝构造的区别
对于左值进行的构造,可以直接进行深拷贝,这使得每个构造出来的类均具有自己的堆空间。而对于使用右值进行的构造,由于右值为临时变量,其内部的堆内存在构造完成后即被释放,这时就可以使用移动构造函数进行构造,直接将右值内的堆空间赋值给新的类对象,并将原有的临时类内的指针=nullptr,这样再执行右值的析构函数时就不会释放掉原本的堆内存,避免了内存的重复创建和释放。
四、默认合成移动构造函数
 1 class A
 2 {
 3 public:
 4     int *p = new int;
 5     string str = "Fuck";
 6 };
 7 
 8 int main()
 9 {
10     A a;
11     A b(std::move(a));
12     cout << a.p << " " << a.str << " ";
13     cout << b.p << " "<<b.str << " ";
14     system("pause");
15 }
结果输出 a.p与b.p地址一样的,而对于a.str显示为空而b.str显示为Fuck.只有string的资源转移了
个人理解:C++编译器合成的默认移动函数对于基本类型所谓移动只是把其值拷贝,对于string这类类成员中因为其类实现了移动构造函数才会真正的所谓资源移动。
另外只要定义了析构函数,那就不会默认生成移动构造函数,因为对于基本类型来说是"复制"而非真正的移动,只要其一析构了,另外一个的资源也就没了.所以只要定义了析构函数,就不会合成移动构造~(刚刚测试了,定义了析构,结果输出了2个fuck,确实是这样)
参考:
posted @ 2018-02-27 20:32  鸭子船长  阅读(1692)  评论(0编辑  收藏  举报