为什么需要将移动构造函数和移动赋值运算符标记为 noexcept
前言
在看 C++ Primer 时注意到这个问题,以下是书中关于这个问题的解释:容器类在扩容时会将旧空间的对象转移到新空间,如果容器不能确定该对象的移动构造函数不会抛出异常的话,就会”谨慎“地使用拷贝构造函数,这样即便过程中发生了异常也能保证不破坏原来容器中的对象(移动一个对象会改变其内容)。
正文
由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。我们将看到,除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
一种通知标准库的方法是在我们的构造函数中指明noexcept。noexcept是新标准引入的,我们将在18.1.4节(第690页)中讨论更多细节。目前重要的是要知道, noexcept是我们承诺一个函数不抛出异常的一种方法。我们在一个函数的参数列表后指定noexcept。在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间。我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept。
1
2
3
4
5
6
7
8
class StrVec{
public:
StrVec(StrVec&& rhs) noexcept;
};
StrVec::StrVec(StrVec&& rhs) noexcept {
}
搞清楚为什么需要noexcept能帮助我们深入理解标准库是如何与我们自定义的类型交互的。我们需要指出一个移动操作不抛出异常,这是因为两个相互关联的事实:首先,虽然移动操作通常不抛出异常,但抛出异常也是允许的;其次,标准库容器能对异常发生时其自身的行为提供保障。例如,vector保证,如果我们调用push_back时发生异常,vector自身不会发生改变。
现在让我们思考push_back内部发生了什么。类似对应的Strvec操作(参见13.5节,第466页),对一个vector调用push_back可能要求为vector重新分配内存空间。当重新分配vector 的内存时,vector将元素从旧空间移动到新内存中,就像我们在reallocate中所做的那样(参见13.5节,第469页)。
如我们刚刚看到的那样,移动一个对象通常会改变它的值。如果重新分配过程使用了移动构造函数,且在移动了部分而不是全部元素后抛出了一个异常,就会产生问题。旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。在此情况下,vector将不能满足自身保持不变的要求。
另一方面,如果vector使用了拷贝构造函数且发生了异常,它可以很容易地满足要求。在此情况下,当在新内存中构造元素时,旧元素保持不变。如果此时发生了异常,vector可以释放新分配的(但还未成功构造的)内存并返回。vector原有的元素仍然存在。
为了避免这种潜在问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。
参考
- C++ Primer 中文版
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!