c++标准库容器提供了什么样的异常安全保证? (强保证 vs 基本保证)

11次阅读

std::vector::push_back的异常安全等级取决于元素类型T的移动构造函数是否为noexcept;若T满足is_nothrow_move_constructible_v,则强异常安全,否则仅基本安全。

c++标准库容器提供了什么样的异常安全保证? (强保证 vs 基本保证)

std::vector::push_back 的异常安全等级取决于什么?

它不总是强异常安全。当 push_back 触发重新分配(即当前容量不足),且元素类型 T 的移动构造函数可能抛出异常时,std::vector 无法保证原状态完全不变——新内存中部分元素可能已构造成功,但旧内存尚未释放,此时若移动构造中途失败,容器会处于“已部分转移”的中间态,只能提供基本异常安全保证。

只有当 T 满足 is_nothrow_move_constructible_v(或退化为拷贝构造且该拷贝也不抛异常)时,push_back 才具备强异常安全保证。

  • 检查方式:
    static_assert(std::is_nothrow_move_constructible_v);
  • 常见陷阱:自定义类未将移动构造函数标记为 noexcept,即使逻辑上不抛异常,标准库仍按可能抛出来处理
  • 替代方案:提前调用 reserve() 避免重分配,此时 push_back 仅需就地构造,只要 T 的构造函数不抛,就满足强保证

std::map::insert 为什么通常有强异常安全保证?

std::map 是基于红黑树实现的节点式容器,插入操作本身不涉及大块内存重分配;每个新节点独立分配,且插入失败(如键已存在)时,新节点直接被销毁,不会影响已有结构。

只要节点分配器(默认 std::allocator)的 allocate 不抛异常(c++17 起 allocatenoexcept),且 value_type 的构造函数不抛,整个 insert 就是强异常安全的。

立即学习C++免费学习笔记(深入)”;

  • 例外情况:使用自定义分配器且其 allocate 可能抛异常 → 降级为基本保证
  • insert 的返回值(pair)在异常发生时仍有效,可安全访问
  • 对比 emplace:语义相同,但若传入的参数构造 value_type 时抛异常,则异常发生在插入前,容器状态完全不变

哪些容器操作只提供基本异常安全保证?

基本保证意味着:异常发生后,容器仍处于有效、可析构的状态,所有对象保持不变(invariant preserved),但可能丢失部分未完成的操作效果(比如部分元素被移除、部分插入失败),且迭代器/引用可能失效。

  • std::vector::resize(n):当 n > size() 且新增元素的默认构造函数抛异常 → 已构造的元素保留,size() 变为实际成功构造的数量(不是原值,也不是目标值)
  • std::deque::push_front:内部多段缓冲区管理复杂,某些实现下扩容失败可能导致已分配的段未清理干净,仅保证容器可安全析构
  • std::list::splice(跨容器):若目标 list 分配器不兼容,可能需要逐个节点移动并重新分配,任一节点移动失败即终止,源和目标 list 状态都有效但内容不确定

如何判断你正在使用的操作是否强异常安全?

不能依赖直觉,必须查标准或实现行为。C++ 标准对每种容器的每个成员函数明确规定了异常安全等级,关键看两点:

  • 操作是否涉及内存重分配(vector/String 扩容、unordered_map 重建桶)
  • Tvalue_type 的相关操作(构造、移动、赋值)是否为 noexcept

例如:std::string::append 在 C++11 中是强异常安全的,前提是字符类型(通常是 char)的构造不抛;但若使用自定义字符类型且其构造函数抛异常,就只保证基本安全。

真正容易被忽略的是分配器行为——哪怕所有类型操作都 noexcept,只要分配器的 allocatedeallocate 可能抛(比如调试版分配器带检查),整个操作就可能降级。生产环境默认分配器虽不抛,但自定义时务必确认 std::allocator_traits::allocate 是否 noexcept

text=ZqhQzanResources