vector与不抛出异常的移动构造
概念说明
C++ Primer中有这个一条提示:不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
C++11引入了移动语义,其中一个重要的概念是移动构造函数。移动构造函数是一个特殊的构造函数,用于将一个对象的资源所有权从一个对象转移到另一个对象,而不是进行复制。这可以提高性能,特别是对于大型对象或需要频繁传递的对象,因此我们希望在合适的时机尽可能使用移动语义。
vector作为C++中常用的标准库容器,大多数人都知道vector有扩容机制,即vector在插入元素时,如果vector的内部存储空间不足时,会自动扩容以容纳更多的元素。具体而言,vector会自动分配一个新的内存块,并将原有的元素复制到新的内存块中。通常,我们希望在扩容复制的时候有更好的性能,因此希望vector能够用到移动语义。
noexcept能够帮助我们进一步理解其中的细节。通常来说,移动操作只是“转移”资源,是不抛出异常的,但抛出异常也是被允许的。另外,标准容器能对异常发生时的自身行为提供保障。比如,vector保证,如果push_back是发生异常,那么vector自身不会发生改变。从vector自身的异常保障机制来看,如果vector在扩容重新分配时使用拷贝构造很容易满足上述需求。当在新的内存块中构造元素时,旧的元素依然存在,如果此时发生异常,vector可以放弃重新分配的内存返回到原始状态,保证了异常发生时不会发生改变。然而,移动构造是一种“窃取”资源的行为,一旦发生异常,旧空间中的元素已经被改变,新空间中的构造尚未完成,难以满足vector自身保持不变的要求。
vector会尽可能的避免上述问题的发生,除非知道元类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造。因此,如果希望vector扩容时会使用移动构造而不是拷贝,我们就需要显式地告知标准库移动构造函数可以安全使用,即通过noexcept来标记为不抛出异常。
代码验证
本文通过一段代码示例验证上述机制。
vector的异常保护
首先,通过一段代码测试如果在扩容期间拷贝构造抛出异常,vector的行为特征。代码如下所示:
#include <iostream>
#include <vector>
using namespace std;
class MyException : public std::exception {
public:
explicit MyException(const std::string& message) : m_message(message) {}
const char* what() const noexcept override {
return m_message.c_str();
}
private:
std::string m_message;
};
class A {
public:
A() { cout << "A constructor" << endl; }
A(const A& a) {
count ++;
cout << "A copy constructor" << endl;
// 当拷贝构造被调用超过1次时抛出异常
if(count > 1){
throw MyException("exception from copy constructor");
}
}
A(A&& a) { cout << "A move constructor" << endl; }
static int count;
};
int A::count = 0;
void printVector(const vector<A>& vA){
cout << "size: " << vA.size() << endl;
for(const auto& a : vA){
cout << &a << " ";
}
cout << endl;
}
int main(){
vector<A> vA;
vA.emplace_back();
vA.emplace_back();
try{
cout << "before exception:" << endl;
printVector(vA);
vA.emplace_back();
} catch(exception & e){
cout << "Exception caught:" << endl;
printVector(vA);
}
return 0;
}
某次的执行结果如下所示:
A constructor
A constructor
A copy constructor
before exception:
size: 2
0x1d620a41a70 0x1d620a41a71
A constructor
A copy constructor
Exception caught:
size: 2
0x1d620a41a70 0x1d620a41a71
其中,第一次emplate_back()时,调用了一次构造函数;第二次emplace_back()时,先调用了一次构造函数,然后因为需要扩容调用了一次拷贝构造;第三次先输出vector的基本信息(此时有先前加入的两个元素),然后emplace_back(),先调用了一次构造函数,然后因为需要扩容调用拷贝构造(超过一次,抛出异常),然后捕获异常后输出vector的基本信息。此时,可以观察到vector处理异常时能够保证其前后的内容不变。
不抛异常的移动构造
为了验证扩容时的调用规则,用以下代码进行测试。其中,类A和类B都有拷贝构造和移动构造,不同的是类B的移动构造不能抛出异常。通过向vector中插入元素,观察其输出结果。
#include <iostream>
#include <vector>
using namespace std;
class A {
public:
A() { cout << "A constructor" << endl; }
A(const A& a) { cout << "A copy constructor" << endl; }
A(A&& a) { cout << "A move constructor" << endl; }
};
class B{
public:
B() { cout << "B constructor" << endl; }
B(const B& b) { cout << "B copy constructor" << endl; }
B(B&& b) noexcept { cout << "B move constructor" << endl; }
};
int main(){
vector<A> vA;
vector<B> vB;
vA.emplace_back();
vA.emplace_back();
vA.emplace_back();
cout << "==========================\n";
vB.emplace_back();
vB.emplace_back();
vB.emplace_back();
return 0;
}
以上代码的运行结果如下:
A constructor
A constructor
A copy constructor
A constructor
A copy constructor
A copy constructor
==========================
B constructor
B constructor
B move constructor
B constructor
B move constructor
B move constructor
类A和类B的执行结果顺序都是一致的,区别在于调用的类方法不同。其中,第一次emplate_back()时,调用了一次构造函数;第二次emplace_back()时,先调用了一次构造函数,然后因为扩容对已有的一个元素进行复制/移动;第三次emplace_back()时,先调用了一次构造函数,同样因为扩容对已有的两个元素进行复制/移动。显然,类B的因为移动构造函数不抛出异常,所以vector扩容时调用了移动构造而不是拷贝构造,从而提升了性能。
Comments | NOTHING