【深度C++】之“对象移动”

0. 什么是对象移动

对象移动解决的问题是一个类含有大量动态内存,在对其进行拷贝时产生的临时内存问题。

如下情况的需求:

class MyIntVector {
private:
    int *elements;
    int *first_free;
    int *cap;
public:
    // 默认构造函数
    MyIntVector();
    // 拷贝构造函数
    MyIntVector(const MyIntVector&);
    // 拷贝赋值运算符
    MyIntVector &operator=(const MyIntVector&);

    // 析构函数
    ~MyIntVector();

    void push_back(const int &);

    // 其他省略...
};

我们观察到,MyIntVector中有三个int *指针,该类的功能与std::vector<int>一样,因此需要我们动态管理内存。

我们先看拷贝系函数拷贝构造函数拷贝赋值运算符),他在如下情况会被调用:

// 声明了一个返回MyIntVector的函数
MyIntVector get_vec(istream &);

// 创建一个实例, 并push一个元素
MyIntVector v1;
v1.push_back(2);

// 这里会调用MyIntVector的
// 拷贝构造函数
MyIntVector v2(v1);

// 调用MyIntVector的
// 拷贝赋值运算符
MyIntVector v3;
v3 = get_vec(cin);

上述代码逻辑上没有任何问题,v1、v2和v3独立管理了三个不相关的内存,对于MyIntVector的使用,和内置类型的对象int、float等一样。

有“问题”的就是第14行,这里有无用的临时内存产生。

我们可以画图将第14行代码的内存过程表示出来。


当函数返回类实例时,由于C++函数值传递的设计,会产生一个临时的返回值,该返回值会调用拷贝构造函数。当这个临时返回值再赋值给v3时,还会调用一次拷贝赋值运算函数。

我们把一个临时量管理的内存内容完整的拷贝了一遍,临时量在作用域结束后就消失了,此时有2次拷贝过程:函数返回初始化一个临时量、临时量初始化v3。

因为临时量之后我们根本无法访问到,那么有没有办法不做这次拷贝,直接让v3获得临时量中动态内存的管理权限?

答案就是对象移动:把动态内存内容的管理权限进行交接,从外部看,仿佛内存内容从一个对象中移动到了另一个对象中,没有拷贝出一份新的内存内容。

1. 怎样实现对象移动

我们怎样获得临时量的控制权呢?这里就要引入C++11的新技术:右值引用

关于右值引用的内容,我们要从左值和右值说起。这两部分内容,请参考【深度C++】之“左值与右值”

总而言之,我们可以使用&&运算符为某类型定义一个右值引用,引用的是表达式的右值结果。右值引用是一个全新的数据类型,它与之前使用&定义的左值引用毫无瓜葛。

在读者了解了右值引用之后,我们接着引入C++11的另一项新技术:移动构造函数移动赋值运算符

2. 移动构造函数

我们通过使用&&,可以为类定义移动构造函数

MyIntVector::MyIntVector(MyIntVector &&src) noexcept
    : elements(src.elements),
      first_free(src.first_free),
      cap(src.cap) {
    src.elements = src.first_free = src.cap = nullptr;
}

2.1 返回值

与其他系的构造函数一样,无返回值。

2.2 函数名

与其他系的构造函数一样,与类类型同名。

2.3 参数

  1. 第一个参数必须是该类类型的非常量右值引用
  2. 任何除了第一个参数的额外参数都必须有默认实参

2.4 函数功能规范

  1. 使用成员初始值列表接管src中的资源
  2. 在函数体中,令src进入这样的状态:对其运行析构函数是安全的

2.5 noexcept

移动构造函数不应该抛出任何异常。

原因:

  1. 移动操作“窃取”资源,不分配资源,一般不会抛出异常
  2. 当自己的类用在容器中(比如在vector<MyIntVector>),在某些情况下,只有声明为noexcept,标准库中的函数才会使用移动构造函数,否则将使用拷贝构造函数替代。

2.6 调用条件

若给类类型定义了移动构造函数,当初始化该类类型的某个对象时传入的是该类类型的一个右值,将会默认使用移动构造函数。例如:

// 声明了一个返回MyIntVector的函数
MyIntVector get_vec(istream &);

// 将使用移动构造函数
MyIntVector v4(std::move(get_vec(cin)));

// 此处注意编译器的问题、理论上不需要使用std::move
// 但编者实际测试下来,必须使用才能调用移动构造函数
// 但显示使用std::move, 下行代码一定会调用移动构造函数
// MyIntVector v4(std::move(get_vec(cin)));

3. 移动赋值运算符

我们通过使用&&,可以为类定义重载了的移动赋值运算符

MyIntVector &MyIntVector::operator=(MyIntVector &&rhs) noexcept {
    // ① 检测自赋值
    if (this != &rhs) {
        // ② 调用类的清理函数,释放已有资源
        ...

        // ③ 移动资源到本类
        this->elements = rhs.elements;
        this->first_free = rhs.first_free;
        this->cap = rhs.cap;

        // ④ 将rhs置于可析构状态
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

3.1 返回值

必须返回该类类型的一个引用,以处理连续赋值

3.2 函数名

  1. 必须定义为成员函数
  2. 函数名为operator=

3.3 参数

只有一个参数作为=的右侧值,必须是该类类型的非常量右值引用

3.4 函数功能规范

  1. 检测自赋值情况
  2. 清理当前对象中管理的资源
  3. 从rhs中接管资源
  4. rhs进入这样的状态:对其运行析构函数是安全的

3.5 noexcept

重载了的移动赋值运算符不应该抛出任何异常。

3.6 调用条件

若给类类型M定义了重载了的移动赋值运算符,当=左侧是M的对象、右侧是M的右值时,就会自动调用移动赋值运算。

// 声明了一个返回MyIntVector的函数
MyIntVector get_vec(istream &);

// 将使用移动赋值运算
MyIntVector v5;
v5 = std::move(get_vec(cin));

4. 合成的移动系函数

拷贝构造函数拷贝赋值运算符一样,编译器也会合成移动构造函数移动赋值运算符

合成移动系函数的条件:

  1. 没有定义任何拷贝系函数
  2. 没有定义析构函数
  3. 他的所有数据成员都能移动构造或移动赋值

可以看到,合成移动系函数的条件十分苛刻。我们通常都会定义析构函数,所以要注意此时是没有移动系函数的。

5. 拷贝 vs 移动

当一个类既有拷贝系函数,又有移动系函数,则编译器在判断函数调用时,会遵循“拷贝左值,移动右值”的原则,即按照一般的函数匹配原则,严格区分左值和右值。

例如,假设之前声明的get_vec函数返回的是MyIntVector的左值引用:

// 声明了一个返回MyIntVector的左值引用的函数
MyIntVector &get_vec(istream &, MyIntVector &);

// 将使用拷贝构造函数
MyIntVector v6 = get_vec(cin);

当一个类没有移动系函数,编译器会调用拷贝系函数完成构造或赋值。

当一个类既没有拷贝系函数,也没有移动系函数,那么编译器会为类自动合成拷贝系函数,所以不用担心拷贝初始化和赋值情况会编译不通过。是否合成移动系函数,要看符不符合第4节提到的条件。

6. 小心编译器

前述都C++标准理论上的内容,经编者测试,某些平台对返回类类型进行了处理,想要使用移动构造函数,必须显示使用std::move才可以,请读者注意。

7. 总结

当某类类型有动态管理内存的需求,在拷贝和赋值该类类型对象时,某些情况下,拷贝源在使用后就被销毁了。

当出现上述情况,使用对象移动操作可以避免二次开销。

通过使用移动构造函数移动赋值运算符进行对象移动。两个函数必须遵守设计规范,并且保证不抛出异常noexcept

编译器也会为类合成移动系函数,但是条件比较苛刻。

拷贝系函数是移动系函数的保底操作,在移动系函数不充足的情况下,会调用拷贝系函数。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值