C/C++编程:移动和异常

1059 篇文章 285 订阅

迁移数据的风险

  • 通常,在复制构造以及赋值函数中都无须对数据来源进行任何改动,所以其参数常常为只读引用形式。这样,即使某个对象在赋值构造函数或者赋值函数中抛出异常,也仅仅意味着构造或更新该数据失败,不会影响来源对象。
  • 但是,在移动构造和赋值函数中,几乎一定会改动数据来源。如果某对象在移动数据时抛出异常,则不仅该对象的构造或更新失败,数据来源对象也可能被改变。如果在对一组数据进行移动构造或者复制的中间出现了异常,则可能之前移动过的数据都会被失败

以std::vector为例,如果压入数据时,预留空间被用光,则会申请更大的空间并将已存数据迁移到新空间内。由于旧空间在迁移完成后会被释放,因此应该用移动构造:

template <typename T>
T * move_constrcut_all(T * begin, T * end, T * dest){
    for(; begin != end; begin++, end++){
        new(dest) T(std::move(*begin));
    }
    return dest;
}

如果类型T在移动过程中抛出异常,带来的后果是不可预计的

noexcept

针对上面的风险,早期C++11标准给出的对策是:禁止移动构造抛出异常。这一做法欠妥,因为:

  • 很难保证所写的每一个移动构造函数中的每一条语句,以及每一个调用的子函数都不会产生异常,尤其是写模板代码时,编译器也不可能去检查移动构造函数所调用的每一个操作是否抛出了异常,这将导致编译时间大大增加
  • 虽然移动构造函数中有可能抛出异常,但绝大部分情况下,其操作就是交换两个空间的数值,很少会需要抛出异常。因为罕见的情形而为移动构造函数整体加以约束,这是不对的。

因此,在新标准中允许移动构造函数抛出异常。同时新增noexcept以及转义函数模板std::move_if_noexcept,辅助程序员写出安全的移动构造函数。

noexcept

  • noexcept用于标记函数不会抛出异常。
  • 当某个函数需要依据其所调用的函数来判断是否会抛出异常时,可以使用noexcept表达式声明。比如模板中是否抛出异常可能取决于模板函数有关的函数调用,这时就可以使用noexcept判断
void foo(){}
void foo_a() noexcept(true){};   // 不抛出异常
void foo_b() noexcept(false){};  // 抛出异常
void foo_c() noexcept (noexcept(foo_a()) && noexcept(foo_b()) ){
    foo_a();
    foo_b();
}

template <typename T>
void bar_d() noexcept (T()){
    // 取决于T的默认构造函数是否会抛出异常
    T a();
}
  • 标准中规定,noexcept表达式除了函数调用之外,还可以为函数指针、成员函数指针、throw表达式、dynamic_cast表达式以及typeid表达式等。

转义函数模板

由于移动构造函数与赋值构造函数将对数据来源进行改动。使得在其中抛出异常所带来的危害要远大于复制构造和赋值中的异常。但是“移动”数据只涉及两对象中的数据,很少需要申请新的资源,也很少需要抛出异常。

可以说,绝大多数移动构造和赋值函数都适合用noexcept标记。但是我们也不能完全无视在移动中抛出异常的情况。为此,标准中新定义了一个转义函数模板std::move_if_noexcept,其功能为:当某类型T具有非抛移动构造函数时,函数模板的T类型参数将被转义为右值引用(T&&),否则被转义为只读左值引用

在一些对数据完整性非常敏感的模板设计中,可以用std::move_if_noexcept代替std::move进行保守的右值移动转义

template <typename T>
T * move_constrcut_all(T * begin, T * end, T * dest){
    for(; begin != end; begin++, end++){
        new(dest) T(std::move_if_noexcept(*begin));
    }
    return dest;
}

如果T的移动构造函数不是非抛函数,move_constrcut_all将赋值构造每个元素而不是移动构造。

移动的效率问题

要利用右值引用使代码运行更快,首要前提是数据类型便于“移动”。而数据类型便于“移动”的前提是,其所管理的大块数据结构由具有指针语义的成员变量索引,否则很难高效“移动”。 比如:

template <typename T, unsigned sz>
struct array0{
    T data[sz];
    array0(){}
    array0(array0 && aa){
        for(unsigned i = 0; i < sz; i++){
            data[i] = std::move(aa.data[i]);
        }
    }
};

template <typename T, unsigned sz>
struct array1{
    T *data;
    array1() : data(new T(sz)){}
    ~array1() {delete data;};
    array1(array1 && aa) : data(new T[sz]){
        std::swap(data, aa.data);
    }
};

这两个数据结构的不同点在于其成员变量data的类型

  • array0:data为数组类型,也即是一个指针常量。由于data值不可变,在其移动构造函数中,无法使data执行右值参数中的数据空间,从而只能将元素一一移动
  • array1:data为指针类型,虽然需要在构造函数与析构函数中为data申请以及释放空间,但在实现移动构造函数时,只需要交换右值参数与自身的data值即可。显然效率更高。

使用右值引用的目的在于:鼓励在类型内部采用独占式成员指针(即一块数据空间只由一个指针所指)来管理大尺寸数据。

数据空间的申请与释放都在类型内部实现,对于类型外部来说,完全无需理会类型内的类型管理。标准库中的容器大部分都采用这样的方式,并且在C++11标准中实现了移动构造与赋值。所以,在自定义数据类型时,可以放心使用标准容器而无须顾虑其移动效率。std::array除外1,其实现方式类型array0,无法快速数据移动,所以慎用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值