C++ 11中增加了名为移动构造函数的构造函数类型。通过使用移动构造函数,我们可以在进行对象复制时直接“窃取”拷贝对象所保有的一些资源,比如已经在原对象中分配的堆内存,文件描述符,IO流等。通常我们在声明移动构造函数时,都会使用 noexcept 关键字来修饰,这样做是为什么,有什么好处呢?
"noexcept"关键字
表示它修饰的函数不会在执行过程中抛出异常。通常使用两种不同的方式来使用这个关键字。第一种方式是将noexcept放在函数声明的后面:
void except_func() noexcept
这样这个函数就会被标记为不会抛出异常。
第二种方式是为noexcept提供一个常量表达式作为参数,如果这个常量表达式的值为true,那么函数就会被标记为不会抛出异常。
constexpr bool suppressExcept = true;
void except_func() noexcept (suppressExcept);
与通常我们使用try...catch结构来捕获异常所不同的是,使用noexcept关键字标记的函数在它抛出异常时,编译器会直接调用名为"std::terminate"的方法,来中断程序的执行,因此某种程度上可以有效阻止异常的传播和扩散。
不仅如此,在C++ 11中类结构隐式自动声明的或者是由程序员主动声明的不带有任何修饰符的析构函数,都会被编译器默认带上noexcept (true)标记,以表示这个析构函数不会抛出异常。这样做是由于,我们希望析构函数的执行只有两种结果,一种是成功的将类对象的资源释放,一种是由于某些原因,导致类资源无法被释放从而直接中断程序的运行。
抛出异常,意味着对于某种错误情况,我们不知道应该怎样处理,因此编译器将其抛给上层调用者来处理,但是析构函数的执行失败,通常会导致比如说资源泄漏或者空指针等潜在问题出现,因此相较于让程序继续运行,一个更加合理的方式是直接终止程序的运行。
另一方面要注意的是,如果一个类的父类析构函数或者它的成员函数被标记为了可抛出异常,那么这个类的析构函数就会默认被标记为可抛出异常,也就我们所说的受到了污染。
接下来看一下noexcept关键字与移动构造函数之间的关系,移动构造函数在对象进行复制时通过直接移动原对象已经分配好的资源,可省去分配内存再拷贝的过程。在STL中大多数容器类型都会在调整容器大小(resize)时调用容器元素的移动构造函数来移动资源,但STL为了保证容器类型的内存安全,在大多数情况下只会调用被标记为不会抛出异常的移动构造函数,否则会调用其拷贝构造函数来作为替代。这是因为在资源的移动过程中如果抛出了异常,那么那些正在被处理的原始对象数据可能因为异常而丢失,而在拷贝构造函数中,由于进行的是资源拷贝操作,原始数据不会被更改,因此即使抛出异常也不会影响数据最终的正确性。
类似的,我们也可以通过给移动构造函数添加noexcept关键字的方式来将它标记为不会抛出异常,使用方式与类成员函数类似,如:
MyClass(MyClass &&res) noexcept;
除此之外,C++ 11还为我们提供了很多标准库方法,可以用于检测类的各种构造函数状态,看下面的例子:
#include <iostream>
#include <type_traits>
using namespace std;
struct ClassA {
int n;
ClassA(ClassA&&) = default;
};
struct ClassB {
int n;
// 标记为可抛出异常
ClassB(ClassB&&) noexcept(false) {};
};
int main(int argc, const char * argv[]) {
cout << is_move_constructible<ClassA>::value << endl
<< is_trivially_move_constructible<ClassA>::value << endl
<< is_nothrow_move_constructible<ClassA>::value << endl
<< is_move_constructible<ClassB>::value << endl
<< is_trivially_move_constructible<ClassB>::value << endl
<< is_nothrow_move_constructible<ClassB>::value << endl;
return 0;
}
-
is_move_constructible
用于检测类型是否可以被移动构造,如果对应的类的移动构造函数被声明为delete,那么这个函数的实例值便为false -
is_trivially_move_constructible
用于检测类型是否具有普通的移动构造函数,为了满足普通这一特点,我们需要保证这个类型符合多项约束条件,比如类型没有虚函数,没有虚基类,没有任何不稳定的静态成员 -
is_move_constructible
用于检测类是否具有不会抛出异常的移动构造函数
总结
合理使用移动构造函数能够使我们开发的应用程序享受到资源移动代替资源拷贝所带来的高性能,但是不合理的使用方式就会导致应用程序的中断,甚至是会在带有诸如内存泄漏数据丢失等潜在风险的情况下继续运行,因为需要在设计类结构时提前考虑到类的各种使用场景,数据在各对象间流动方向,以及对应的处理方式。除此之外,还可以通过设置规范,也就是convention的方式来主动规避可能带来的数据和代码问题。比如说,严格禁止在代码中使用拷贝构造函数以及拷贝赋值函数,使用placement new来初始化堆对象,进而避免数据的拷贝和移动等等。