C++ 拷贝控制(二) — 移动构造函数和移动赋值运算符

相关文章:

  • C++ 拷贝控制(一) — 析构函数、拷贝构造函数与拷贝赋值函数
  • C++ 引用类型 — 左值引用、常引用和右值引用

移动语义

移动语义是 C++ 新标准所引入的一个新的概念,和拷贝语义相对。以拷贝赋值为例,在拷贝的对象的过程当中,为了保证拷贝过程是异常安全的,我们往往需要以下三个步骤:

  • 将赋值运算符左侧的运算对象(以下简称左对象)用局部对象 tmp 保存下来
  • 将左对象析构
  • 将临时变量拷贝到右对象当中。

而在这一过程当中,局部变量 tmp 的作用仅仅只是作为中转站,在拷贝开始前创建,拷贝结束后销毁,成为了制约拷贝效率的一个短板。而 C++ 新标准中所引入的移动语义,则能够将一个对象转移到另一个对象当中,而避免了不必要的局部对象的创建、拷贝和销毁操作。通俗地说,拷贝构造函数和拷贝赋值运算符为对象提供了 “拷贝” 功能,而移动构造函数和移动赋值运算符则为对象提供了 “剪切” 的功能

C++ 中的移动语义通过右值引用来实现,右值引用不仅延长临时对象的生命周期,还可以直接获取临时对象的全部状态。借由移动语义,我们可以将临时对象转移至其他对象当中,从而大大提高了效率。一个比较典型的例子是swap 函数的实现:

template 
void swap(T &a, T &b){
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

移动构造函数

移动构造函数的定义如下:

class MyClass{
public:
    ...
    //移动构造函数定义方式
    MyClass(MyClass &&obj) noexcept:p_str(obj.p_str){
        //将移动源对象置于析构安全状态
        obj.p_str = nullptr;
    }
    ...
private:
    string* p_str;
};
int main(void){
    ...
    MyClass a;
    MyClass b = a;              // 拷贝构造方式
    MyClass c = std::move(a);   // 移动构造方式
    ...
    return 0;
}

从例子中可以看出,移动构造函数是一个以右值引用为参数的构造函数。对于一个移动构造函数,我们应当注意以下两点:

  1. 移动构造函数通常被声明为“无异常抛出”的函数。由于移动操作可以 “窃取” 源对象的状态,因此不涉及资源分配,故一般不抛出异常。此外,当进行拷贝时,由于源对象的状态不发生改变,因此当异常发生时,只需要释放新分配的资源即可,而移动对象会改变源对象的状态,因此出于安全性考虑,编译器在移动构造函数和移动赋值运算符没有显式声明为 “无异常抛出” 时,会在移动过程中自动调用拷贝构造函数和拷贝赋值运算符。
  2. 移动后可能需要对源对象进行析构,我们需要将源对象设置为可安全析构的状态,这样才能防止释放源对象后对移动后对象产生影响。

移动赋值运算符

移动赋值运算符的定义和使用方式如下

class MyClass{
public:
    ...
    //移动赋值运算符定义方式
    MyClass& operator=(MyClass &&obj)noexcept {
        if(this != &obj){
            p_str = obj.p_str;
            obj.p_str = nullptr;
        }
        return *this;
    }
    ...
private:
    string* p_str;
};
int main(void){
    ...
    MyClass a;
    MyClass b,c
    b = a;              // 拷贝赋值方式
    c = std::move(a);   // 移动赋值方式
    ...
    return 0;
}

基于同样的理由,我们通常也会将移动赋值运算符声明为 “无异常抛出” 的,同时我们也需要考虑自赋值以及移动后将源对象置于可析构的有效状态。在使用上,使用移动赋值运算符需要用 std::move 函数来将左值引用转化为右值引用。编译器会根据函数匹配的结果找到合适的运算符。

默认移动构造函数与默认移动赋值运算符

移动构造函数和移动赋值运算符的默认生成规则如下:

生成规则:

  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且所有的非 static 数据成员都能进行移动构造和移动赋值时,编译器才会自动生成移动构造函数和移动赋值运算符

删除规则:

  • 当有类成员定义了自己的拷贝操作而没有定义移动操作,或者编译器不能为类成员生成默认移动操作时,该类的移动操作是删除的;
  • 若类成员的移动操作是删除的或不可访问的,则类的移动操作也被定义为删除的;
  • 类的析构函数被定义为删除的或是不可访问的,则类的移动构造函数被定义为删除的
  • 类成员是 const 或者引用类型,则类的移动赋值运算符被定义为删除的。

注:移动操作 = 移动构造函数 + 移动赋值运算符

总结

  • C++ 新标准中引入了移动语义,能够为对象提供 “剪切” 的功能,这样可以避免不必要的临时对象的创建、拷贝和析构过程,大大提高了效率。移动语义通过右值引用来实现
  • 移动构造函数和移动赋值运算符通常需要被显式声明为 “无异常抛出” 的,否则编译器会出于安全性的考虑,在“剪切”对象的过程中使用拷贝功能。此外,移动的后还应当将移动源对象设置为析构安全的状态,以避免移动源对象析构后导致移动后对象访问异常。
  • 对于移动构造函数和移动赋值运算符而言,一旦类定义了拷贝构造函数和拷贝赋值运算符,则编译器不会自动生成移动构造函数和移动赋值运算符

你可能感兴趣的:(C++ 拷贝控制(二) — 移动构造函数和移动赋值运算符)