C++异常安全
异常安全是指在发生异常时,程序能够保持容器或数据结构的有效性,避免资源泄漏,并保证容器状态的一致性。常见的异常安全级别有基本保证、强保证和不变性保证。
C++异常安全
异常安全(Exception Safety) 是指在程序中发生异常时,代码能够保证某些特定的不变性,确保程序的行为不会变得不可预测或导致资源泄漏。异常安全性使得在出现异常时,程序能够以预期的方式恢复或终止,不会产生副作用。
异常安全的核心目标
- 保持程序的状态一致性:在异常发生时,程序应保持处于有效状态,避免未定义行为。
- 避免资源泄漏:确保分配的资源(如内存、文件句柄、锁等)能够正确释放。
- 保证不产生副作用:即使发生异常,程序不应对外部环境产生不良影响。
异常安全的级别
C++ 标准库定义了几种常见的异常安全级别,开发者在设计函数时要考虑不同的安全性要求:
无异常保证(No-throw guarantee):
- 即使抛出异常,函数也不保证任何安全性。发生异常时,可能导致资源泄漏或不一致的状态。
1 2 3
void unsafeFunction() { // 可能抛出异常,但没有保证 }
基本保证(Basic Guarantee):
- 在发生异常时,函数保证不会造成程序状态的不一致,即保证不泄漏资源,但不能保证数据的具体值是有效的。
- 如果异常发生,资源会被释放,程序不会处于错误状态,但可能会产生部分修改的数据。
1 2 3 4
void basicGuarantee() { std::vector<int> v; v.push_back(1); // 如果此时发生异常,vector v 会被正确析构 }
强保证(Strong Guarantee):
- 强保证意味着在发生异常时,程序的状态不会改变。即,调用此函数之前的所有状态都能保持不变(即使发生异常,调用函数的效果会像没有发生过一样)。
- 这要求在实现过程中使用一些技巧,如回滚操作、复制机制等。
1 2 3 4 5
void strongGuarantee() { std::vector<int> v = {1, 2, 3}; std::vector<int> backup = v; // 保存一个备份 v.push_back(4); // 如果发生异常,v 会恢复到备份状态 }
不抛出异常保证(No-throw Guarantee):
- 这种保证意味着函数不会抛出任何异常。通常在非常关键的操作中要求此保证。
- 比如,
swap()
在 C++ 中被设计为无异常抛出,因为它保证不会引发异常。
1 2 3 4 5
void noThrowGuarantee() noexcept { int a = 1; int b = 2; std::swap(a, b); // 该操作不会抛出异常 }
异常安全的实现技巧
RAII
RAII(资源获取即初始化):通过对象的构造函数和析构函数管理资源,可以确保即使发生异常,资源也能被正确释放。
1
2
3
4
5
6
7
8
9
10
11
12
class FileGuard {
public:
FileGuard(const std::string& filename) {
file = fopen(filename.c_str(), "w");
if (!file) throw std::runtime_error("File open failed");
}
~FileGuard() {
if (file) fclose(file);
}
private:
FILE* file;
};
复制-交换惯用法
复制-交换惯用法(Copy-and-Swap Idiom):通过使用拷贝构造和拷贝交换(copy-and-swap)实现强保证,保证异常安全。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& other) {
// 假设我们进行深拷贝(这里只是一个简单的例子)
data = new int(*other.data);
}
// 赋值运算符(实现拷贝交换)
MyClass& operator=(MyClass other) noexcept {
swap(*this, other); // 使用 swap 交换内容
return *this;
}
// 交换函数
friend void swap(MyClass& first, MyClass& second) {
using std::swap;
swap(first.data, second.data); // 交换资源
}
// 析构函数
~MyClass() {
delete data; // 释放资源
}
private:
int* data;
};
- 复制-交换惯用法的核心思想是通过传值方式接收参数,并且在函数内部通过
swap
操作确保交换对象的状态。这使得赋值操作即使在发生异常时也能保证强保证。
为什么能保证强异常安全?
传值接收对象:
1
MyClass& operator=(MyClass other) noexcept {
- 这里
operator=
通过传值接收MyClass other
参数。这意味着当operator=
被调用时,会先创建other
的副本。即使在构造副本的过程中抛出异常,原始对象(*this
)的状态也不会受到影响。 - 由于传值的过程中会调用
MyClass
的拷贝构造函数(或移动构造函数),即使在构造过程中发生异常,也只会影响临时副本other
,不会影响已经存在的*this
。
swap
操作:
1
swap(*this, other);
swap
操作是原子性的,即使发生异常,它也保证:- 交换操作之前,
*this
仍然保持其原有的状态。 - 如果发生异常,原始的
*this
对象和副本other
的状态都可以通过swap
恢复到之前的一致性状态。
- 交换操作之前,
- 如果
swap
期间发生异常,*this
和other
都处于未修改的状态。也就是说,不会有部分赋值发生,整个赋值操作被“回滚”到原先状态。
noexcept
保证:
operator=
声明为noexcept
,意味着此函数在执行过程中不会抛出任何异常。noexcept
确保了swap
操作也不会抛出异常(前提是swap
本身也是noexcept
的)。
STL 容器的异常安全性
1. std::vector
插入(
push_back
、insert
):std::vector
在内存不足时会进行 重新分配,这时会发生 内存重新分配操作,并将原有元素移动到新的位置。这个过程可能会抛出异常(比如 内存分配失败 或 元素的拷贝/移动构造抛出异常)。因此,push_back
和insert
的异常安全性是 基本保证,即可能修改容器,但不会导致资源泄漏。注意:如果在容器中使用的是移动语义(如使用
std::move
),则可以提高效率,但可能会破坏原容器中的某些元素。删除(
erase
):std::vector
的erase
操作通常是 基本保证,它会删除元素并移动后续元素。在移动或销毁元素时,可能会抛出异常,但不会导致内存泄漏。重分配(
resize
、reserve
):类似push_back
,如果发生 重新分配,容器会将现有元素移动到新位置,这个过程可能会抛出异常。
2. std::list
- 插入(
push_back
、insert
):std::list
是一个双向链表,插入操作通常不会导致内存重新分配,因此具有 强保证。如果插入的元素的拷贝构造或移动构造抛出异常,那么只有当前插入的元素会失败,其他部分保持不变。 - 删除(
erase
):同样,删除操作的异常安全性通常是 强保证,因为它只会影响当前元素的删除,其他元素不会受到影响。 - 节点分配(
resize
):std::list
不会像std::vector
一样需要重新分配大量内存,因此删除和插入的操作较为简单。它们通常能够提供 强保证。
3. std::map
和 std::set
- 插入(
insert
):在std::map
或std::set
中插入元素时,可能会发生内存分配或重新平衡树结构。这些操作通常能够提供 强保证,即如果插入失败,容器的状态将保持不变。 - 删除(
erase
):erase
操作通常不会抛出异常,且 强保证,因为它只会影响单个元素的删除,容器的其他部分保持一致。 - 查找(
find
):find
操作不会抛出异常,因此提供 不变性保证。
4. std::deque
std::deque
是一个双端队列,虽然它的实现与std::vector
类似,但它不像std::vector
那样每次都重新分配全部内存,而是将数据分块存储。其异常安全性类似于std::vector
,但要考虑到块级别的重新分配。- 插入:如果
std::deque
需要重新分配内存或扩展块,可能会导致异常。操作提供的是 基本保证。 - 删除:通常提供 强保证。
- 插入:如果
5. std::unordered_map
和 std::unordered_set
- 插入(
insert
):插入时可能会重新哈希,发生内存分配。插入的异常安全性是 基本保证,因为插入时可能会抛出异常,但不会造成内存泄漏。 - 删除:删除操作通常不会抛出异常,因此能够提供 强保证。
- 查找:
find
操作不会抛出异常,因此是 不变性保证。
小结
std::vector
:对于大部分操作提供 基本保证,但对于push_back
等操作,内存重新分配时可能会抛出异常。std::list
:对于插入和删除操作提供 强保证,因为它不需要大规模的内存重新分配。std::map
和std::set
:对于插入和删除操作通常提供 强保证,特别是在平衡树操作时。std::deque
:类似于std::vector
,提供 基本保证,但比vector
更加复杂。std::unordered_map
和std::unordered_set
:插入操作通常提供 基本保证,删除和查找提供 强保证 和 不变性保证。
总结
异常安全性保证了在程序抛出异常时,资源能够被正确管理,程序不会处于不一致或不可靠的状态。在实现异常安全的代码时,常常使用 RAII、回滚机制、复制-交换惯用法等技巧来实现不同的异常安全级别。
通过合理的设计和异常安全保证,程序能够在异常情况下保持稳定运行,避免数据损坏和资源泄漏。