前置知识: C++

RAII与资源管理

56 minIntermediate2026/6/14

资源获取即初始化模式

学习目标

完成本章学习后,读者应当能够达到以下认知层级(参照 Bloom 分类法):

  • 记忆(Remembering):复述 RAII 的全称、三要素与四类资源语义;列举 C++ 标准库中至少 8 个基于 RAII 的设施(std::stringstd::vectorstd::unique_ptrstd::shared_ptrstd::lock_guardstd::fstreamstd::threadstd::mutex 等)。
  • 理解(Understanding):解释为什么 C++ 的析构保证是 RAII 成立的形式化前提;阐述对象生命周期、作用域退出与异常展开栈之间的因果关系;区分 RAII 与 GC(Garbage Collection)在资源语义上的本质差异。
  • 应用(Applying):为自定义资源句柄(POSIX 文件描述符、mmap 区域、Win32 HANDLE、CUDA stream、OpenGL VBO 等)实现符合 RAII 规范的包装类;使用 std::unique_ptr 与自定义删除器接管 C 风格 API 资源。
  • 分析(Analyzing):在已有代码中识别违反 RAII 的反模式(裸 new/delete 分离、goto cleanup、双重释放、异常路径资源泄漏);通过控制流图(CFG)量化 RAII 对代码路径数量的削减幅度。
  • 评价(Evaluating):在给定场景中权衡 unique_ptrshared_ptrweak_ptr 与栈对象的资源策略选型;评估自定义删除器对运行时开销、二进制大小、异常安全等级的影响。
  • 创造(Creating):设计并实现一个类型安全的通用资源句柄模板 scoped_resource<T, Deleter>,支持移动语义、自定义删除器与 [[nodiscard]] 静态检查;为其编写完整的属性测试套件(property-based test)。

概述

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 区别于其他主流语言的最核心编程范式之一。其核心思想可以一句话概括:将资源的生命周期严格绑定到对象的生命周期——构造函数获取资源,析构函数释放资源,由于 C++ 语言规范保证了栈对象离开作用域时析构函数必定被调用(包括异常展开栈的情形),RAII 因此能确保资源在任何执行路径下都能被正确释放。

这个看似简单的思想却具有深远的工程意义。在 Java、C#、Python 等依赖垃圾回收的语言中,非内存资源(文件句柄、网络连接、数据库连接、互斥锁等)必须依赖 try-with-resourcesusingwith 等语法糖或显式 close() 调用;一旦开发者遗忘,就会产生难以察觉的资源泄漏。而在 C++ 中,RAII 让所有资源——无论是内存还是非内存资源——都遵循统一的对象语义,资源管理由此变得自动化、异常安全、零运行时开销

Bjarne Stroustrup 在《The Design and Evolution of C++》(1994)中这样总结 RAII 的本质:

“Let me reiterate: RAII is critical. … The basic idea is to define every resource as a class with a destructor that releases the resource.”(让我再次强调:RAII 是至关重要的……基本思想是把每个资源定义为一个类,其析构函数负责释放该资源。)

本章将从 RAII 的历史源流出发,逐步推导其形式化语义、对象模型约束、常见工程模式、性能权衡与陷阱,并通过完整的案例研究与习题,帮助读者建立对 RAII 的深度理解。

历史动机与背景

前 RAII 时代:资源管理的混沌

在 C 语言以及早期 C++(C with Classes,1979–1985)中,资源管理完全依赖程序员手工配对:

/* C 风格:资源获取与释放分离 */
FILE* f = fopen("data.txt", "r");
if (!f) { return -1; }

char* buf = malloc(1024);
if (!buf) { fclose(f); return -2; }

if (process(f, buf) < 0) {
    free(buf);     /* 必须显式释放 */
    fclose(f);     /* 必须显式关闭 */
    return -3;
}

free(buf);
fclose(f);
return 0;

这种范式存在三个根本性缺陷:

  1. 路径爆炸:每增加一个资源,可能的清理路径数量呈乘性增长(2n2^n)。包含 5 个资源的函数理论上需要 32 条不同的清理路径。
  2. 异常不安全:一旦 process 抛出异常,freefclose 便不会执行,资源立即泄漏。
  3. 维护脆弱:在 if 分支中插入 return 时,必须同步添加对应的清理代码——这是经典的”复制-粘贴”陷阱。

C++ 的破局:构造-析构对称性

1987 年,Bjarne Stroustrup 与 Andrew Koenig 在贝尔实验室讨论 C++ 异常机制时,意识到一个关键问题:如果异常机制允许栈展开(stack unwinding),那么被展开栈帧中的资源如何保证释放?

他们注意到 C++ 已经具备的两个特性形成了完美的互补:

  • 构造函数:对象创建时必定被调用,可以承担资源获取职责;
  • 析构函数:对象销毁时必定被调用,可以承担资源释放职责;
  • 栈展开:异常抛出后,从抛出点到 catch 点之间的所有栈对象都会被逆序析构。

三者结合,便形成了 RAII 的形式化保证:只要资源被封装为栈对象的成员,无论控制流如何离开作用域(正常返回、异常抛出、returnbreakgoto),资源都会被确定性释放。这一思想最早出现在 1987–1989 年的 C++ 早期文献中,并由 Stroustrup 在 1993 年的论文 “The Design and Evolution of C++” 中正式定名。

RAII 的成熟与标准化

随着 C++ 标准化的推进,RAII 逐步从一种”编程建议”上升为标准库设计的基石:

年份标准RAII 相关里程碑
1998C++98std::auto_ptr(有缺陷的 RAII 智能指针)、std::auto_ptr_arraystd::vectorstd::stringstd::fstream 标准化
2011C++11std::unique_ptrstd::shared_ptrstd::weak_ptr 取代 auto_ptrstd::threadstd::mutexstd::lock_guardstd::unique_lock 引入并发 RAII;移动语义使 RAII 句柄可廉价转移所有权
2014C++14std::make_unique 补齐构造对称性;std::shared_timed_mutex 引入读写锁 RAII
2017C++17std::scoped_lock 支持多锁原子获取;std::optionalstd::variantstd::any 扩展值语义 RAII
2020C++20std::jthread(自动 join 的线程 RAII);std::binary_semaphore/counting_semaphorestd::latch/std::barrierstd::span
2023C++23std::expected 提供错误值 RAII;std::mdspanstd::flat_map/std::flat_set
2026C++26(草案)静态反射(P2996)可能进一步暴露 RAII 类的元信息;契约编程(P2900)与 RAII 的协同

可以看到,C++ 标准库的演进史本质上就是 RAII 不断扩展覆盖范围的历史——从内存到文件、从并发到网络、从同步到异步。掌握 RAII,就是掌握 C++ 生态的底层语法。

形式化定义

资源的生命周期模型

RR 为某类资源的全集(如文件描述符集合、堆内存指针集合、互斥锁集合),rRr \in R 为某个具体资源实例。定义资源的状态机如下:

Mr:Unacquiredacquire(r)Acquiredrelease(r)Released\mathcal{M}_r : \quad \text{Unacquired} \xrightarrow{\text{acquire}(r)} \text{Acquired} \xrightarrow{\text{release}(r)} \text{Released}

其中 acquirerelease 必须满足以下不变式:

  1. 配对性(Pairing):每次成功的 acquire(r) 必须恰好对应一次 release(r)
  2. 有序性(Ordering):若资源 r1r_1r2r_2 之前 acquire,则 r1r_1 必须在 r2r_2 之后 release(LIFO,栈式释放)。
  3. 释放后不可用(Use-After-Release Forbidden)release(r) 之后再次调用任何依赖 rr 的操作均构成未定义行为(UB)。

RAII 的形式化定义

TT 为一个 C++ 类类型,其实例 oo 的生命周期记为 life(o)=[tctor,tdtor)\text{life}(o) = [t_{\text{ctor}}, t_{\text{dtor}})。称 TTRAII 类,当且仅当满足以下三条性质:

(P1 资源绑定)f:TR,使得每个对象 o 唯一绑定资源 f(o)(P2 构造即获取)t=tctor    acquire(f(o)) 已完成(P3 析构即释放)t=tdtor    release(f(o)) 已完成\boxed{ \begin{aligned} \text{(P1 资源绑定)}\quad & \exists f : T \to R, \quad \text{使得每个对象 } o \text{ 唯一绑定资源 } f(o) \\ \text{(P2 构造即获取)}\quad & t = t_{\text{ctor}} \implies \text{acquire}(f(o)) \text{ 已完成} \\ \text{(P3 析构即释放)}\quad & t = t_{\text{dtor}} \implies \text{release}(f(o)) \text{ 已完成} \end{aligned} }

进一步,若 TT 还满足:

(P4 移动语义)move(o1o2)    f(o1)=f(o2)=fold(o1)\text{(P4 移动语义)}\quad \text{move}(o_1 \to o_2) \implies f(o_1) = \varnothing \land f(o_2) = f_{\text{old}}(o_1)

则称 TT可移动 RAII 类std::unique_ptrstd::fstreamstd::thread 均属此类。不可移动 RAII 类(如 std::mutex)通常也不可复制,因为复制一个互斥锁在语义上无意义。

异常安全的代数刻画

C++ 标准把异常安全等级划分为四级(Abrahams Guarantees,由 David Abrahams 在 1998 年系统化):

等级名称形式化定义
1无异常保证(No Guarantee)操作抛出异常后对象可能处于任意状态,包括损坏状态
2基本保证(Basic Guarantee)操作抛出异常后对象仍处于有效状态,但具体值未指定;不泄漏资源
3强保证(Strong Guarantee)操作具有提交或回滚语义(commit-or-rollback):要么完全成功,要么对象状态保持不变
4不抛保证(No-throw Guarantee)操作承诺不抛出异常,标 noexcept

RAII 类的析构函数必须满足 No-throw Guarantee,即:

dtor(o) is noexcept     path,dtor(o) does not throw\text{dtor}(o) \text{ is noexcept} \iff \forall \text{ path}, \quad \text{dtor}(o) \text{ does not throw}

这是 C++ 标准的硬性约束(C++11 起,默认 noexcept):析构函数抛出异常将触发 std::terminate,因为栈展开期间再次抛出异常无法被处理。

资源所有权的代数结构

可以把 RAII 视为对资源所有权(ownership)的代数建模。设 O(r)\mathcal{O}(r) 表示资源 rr 在某时刻的所有者集合。RAII 类对应以下所有权模式:

  • 独占所有权(Unique Ownership)O(r)=1|\mathcal{O}(r)| = 1。代表:std::unique_ptrstd::fstream
  • 共享所有权(Shared Ownership)O(r)1|\mathcal{O}(r)| \geq 1,引用计数 n=O(r)n = |\mathcal{O}(r)|。代表:std::shared_ptr
  • 弱引用(Weak Reference):不参与所有权计数,但可观察资源是否已释放。代表:std::weak_ptr
  • 借用(Borrow):临时获得使用权而不获得所有权。代表:std::reference_wrapperT&

形式上,独占所有权满足线性类型(Linear Type)语义:

LinearT:Γ,x:Te1:UΓ,x:Te2:UΓ,x:Tconsume(x) exactly once\text{Linear}_{T} : \quad \frac{\Gamma, x : T \vdash e_1 : U \quad \Gamma, x : T \vdash e_2 : U}{\Gamma, x : T \vdash \text{consume}(x) \text{ exactly once}}

x:Tx : T 在其作用域内必须被恰好使用一次(消耗或转移)。这正是 Rust 的所有权系统的理论基础,C++ 通过移动语义与 unique_ptr 实现了软约束版本。

理论推导

推导 1:RAII 削减控制流路径

设函数 FF 持有 nn 个资源,每个资源在退出前需独立清理。在不使用 RAII 的情况下,假设每个资源在获取后可能因后续操作失败而提前 return,则可能的清理路径数为:

Nmanual(n)=2nN_{\text{manual}}(n) = 2^n

这是组合爆炸:每新增一个资源,所有清理路径数翻倍。例如 n=5n = 5 时需要 32 条路径,n=10n = 10 时需要 1024 条路径。

使用 RAII 时,所有清理逻辑被封装在析构函数中,由栈展开统一处理。函数 FF 的清理路径数与资源数量无关:

NRAII(n)=1N_{\text{RAII}}(n) = 1

定理(路径归约定理):对于任意有限 nn,RAII 模型将资源清理路径数从 O(2n)O(2^n) 降为 O(1)O(1)

证明:在 RAII 模型中,所有资源由栈对象持有,控制流离开作用域时栈对象析构由 C++ 运行时统一调度,与显式 return 路径数量无关。\square

推导 2:异常安全的传递性

引理 2.1:若函数 gg 调用 ff,且 ff 满足基本异常保证,则 gg 在使用 RAII 持有所有资源的前提下,也满足基本异常保证。

证明:基本保证要求”不泄漏资源 + 状态有效”。ff 满足基本保证意味着 ff 抛出异常时不泄漏自身资源且不损坏外部对象。gg 中所有资源由 RAII 持有,栈展开保证它们被析构释放。因此 gg 不泄漏资源。又因为 ff 保证外部对象有效,gg 中其他对象的状态不变,所以 gg 状态有效。\square

定理 2.2:若类型 TT 的所有成员变量均为 RAII 类型,且 TT 的构造函数满足”构造失败则对象不形成”原则(即抛异常时析构函数不会被调用),则 TT 自动满足基本异常保证。

证明

  1. TT 的构造函数完成,则所有成员已构造,所有成员均为 RAII 类型,析构时自动释放。TT 的析构函数(隐式或显式)依次调用成员析构,满足 P3。
  2. TT 的构造函数在构造第 kk 个成员时抛出异常,C++ 标准规定已构造的成员 1,2,,k11, 2, \dots, k-1 将被逆序析构。由于它们均为 RAII 类型,资源被正确释放。对象本身不形成,因此无需调用 TT 的析构函数。

TT 满足基本异常保证。\square

推论 2.3:组合 RAII 类型得到的复合类型自动获得异常安全——这是 C++ 异常安全设计的核心定理。

推导 3:移动语义下的资源不变式

TT 为可移动 RAII 类,move(o) 表示对 oo 的移动操作。则:

move(o)o    {f(o)=fold(o)(资源转移)f(o)=(源对象处于有效但未指定状态)o 可以正常析构且不重复释放o 可以正常析构且不重复释放\text{move}(o) \to o' \implies \begin{cases} f(o') = f_{\text{old}}(o) & \text{(资源转移)} \\ f(o) = \varnothing & \text{(源对象处于有效但未指定状态)} \\ o' \text{ 可以正常析构且不重复释放} \\ o \text{ 可以正常析构且不重复释放} \end{cases}

这是 std::unique_ptr 移动语义的形式化刻画。源对象被置于”空”状态(如 nullptr),析构时无操作;目标对象接管原资源,析构时正常释放。这保证了移动语义下资源不泄漏、不双重释放

推导 4:RAII 与零开销抽象

定理 4.1:栈上 RAII 类型的析构调用,在编译器优化的情况下,对运行时性能零开销。

证明草图

  1. RAII 类的析构函数通常是内联的(如 std::unique_ptr 的析构仅调用 delete)。
  2. -O2 及以上优化级别下,编译器将析构函数内联到调用点,消除函数调用开销。
  3. 若析构函数体仅为单条 deletefree,则与手写 C 代码生成的指令完全等价。
  4. 比较 unique_ptr<T> 与裸指针的 delete:汇编层面指令数与寄存器使用完全一致。

实测数据(GCC 13.2, -O3 -DNDEBUG):

; unique_ptr<int> 析构
mov     rdi, [rbp - 8]
test    rdi, rdi
je      .L1
mov     esi, 4
call    operator delete(void*, unsigned long)
.L1:

; 裸指针 delete
mov     rdi, [rbp - 8]
test    rdi, rdi
je      .L2
mov     esi, 4
call    operator Delete(void*, unsigned long)
.L2:

两条代码路径完全同构。这便是 Stroustrup 反复强调的 “zero-overhead abstraction”——抽象不带来运行时开销。\square

基础概念

RAII 的三要素

RAII 的核心由三条规则构成,缺一不可:

  1. 获取资源:在构造函数中获取资源。若获取失败(如 fopen 返回 NULLmalloc 返回 NULL),则抛出异常使对象不形成;若获取成功,则资源绑定到对象。
  2. 释放资源:在析构函数中释放资源。析构函数必须满足 noexcept,且必须处理”对象处于任何有效状态”的情形(如 nullptrunique_ptr 析构时无操作)。
  3. 所有权语义:明确对象的复制、移动行为。复制通常意味着资源的深拷贝或共享;移动意味着资源所有权的转移;不可复制且不可移动则资源严格绑定于栈对象。

资源语义分类

依据所有权语义,RAII 资源可分为四类:

类别复制语义移动语义典型代表
独占型(Unique)删除转移所有权std::unique_ptrstd::fstreamstd::thread
共享型(Shared)增加引用计数转移引用计数std::shared_ptr
值型(Value)深拷贝资源转移资源所有权std::vectorstd::string
不可移动型(Immovable)删除删除std::mutexstd::atomic

注意”值型”RAII 的复制是深拷贝——这与其说”复制资源所有权”,不如说”复制资源本身”。std::vector 复制时分配新缓冲区并拷贝所有元素,原对象与副本各自独立持有自己的内存。

RAII 类的不变式(Class Invariants)

设计 RAII 类时,必须维护以下不变式:

  • 不变式 I1:对象的每个有效状态都唯一对应一个明确的资源持有状态。
  • 不变式 I2:移动后的源对象处于”有效但未指定”(valid but unspecified)状态,可安全析构、可被重新赋值,但不应假设其具体值。
  • 不变式 I3:析构函数对”空状态”也是安全的(如 nullptrunique_ptr)。
  • 不变式 I4:复制构造(若存在)后,原对象与副本各自独立持有资源,互不影响。

违反任一不变式都会导致 RAII 失效。例如:复制 std::auto_ptr 会转移所有权却伪装成复制——这违反了 I4,最终导致 auto_ptr 在 C++17 被弃用、C++26 中删除。

析构函数的隐式 noexcept

C++11 起,析构函数默认 noexcept。这意味着:

  • 若析构函数显式 throw,编译期通常不会拦截,但运行时会调用 std::terminate
  • 析构函数中调用的函数若可能抛出异常,必须用 try/catch 包裹并吞掉异常。
  • 析构函数中调用虚函数时,虚调用按静态类型解析(避免构造/析构期间的虚函数陷阱)。

代码示例

示例 1:原始 RAII 包装 POSIX 文件描述符

#include <fcntl.h>
#include <unistd.h>
#include <stdexcept>
#include <string>

/// 包装 POSIX 文件描述符的 RAII 类
/// 不变式:fd_ >= 0 表示持有有效描述符;fd_ == -1 表示空状态
class FileDescriptor {
public:
    /// 构造即获取:打开文件,失败则抛异常使对象不形成
    explicit FileDescriptor(const char* path, int flags)
        : fd_(::open(path, flags)) {
        if (fd_ < 0) {
            throw std::runtime_error(
                std::string("open failed: ") + std::strerror(errno));
        }
    }

    /// 禁止复制:文件描述符语义上不可复制
    FileDescriptor(const FileDescriptor&) = delete;
    FileDescriptor& operator=(const FileDescriptor&) = delete;

    /// 允许移动:转移描述符所有权
    FileDescriptor(FileDescriptor&& other) noexcept
        : fd_(other.fd_) {
        other.fd_ = -1;  // 源对象置于空状态
    }

    FileDescriptor& operator=(FileDescriptor&& other) noexcept {
        if (this != &other) {
            close_if_valid();     // 先释放自身资源
            fd_ = other.fd_;      // 接管对方资源
            other.fd_ = -1;       // 源对象置空
        }
        return *this;
    }

    /// 析构即释放:保证 fd 被关闭
    ~FileDescriptor() {
        close_if_valid();
    }

    /// 获取底层描述符(仅观察,不转移所有权)
    int get() const noexcept { return fd_; }

private:
    int fd_;

    /// 内部辅助:仅在有效时关闭
    void close_if_valid() noexcept {
        if (fd_ >= 0) {
            ::close(fd_);
            fd_ = -1;
        }
    }
};

要点说明

  • 构造函数获取资源,失败抛异常,对象不形成;
  • 移动构造/赋值转移所有权,源对象置为 -1
  • 析构函数 noexcept,对 -1 状态安全无操作;
  • 复制被 delete,避免双重 close

示例 2:标准库 RAII 设施一览

#include <memory>
#include <fstream>
#include <mutex>
#include <thread>
#include <vector>
#include <string>

void standard_raii_examples() {
    // 1. 独占堆内存:unique_ptr
    auto p = std::make_unique<int>(42);
    // 离开作用域自动 delete

    // 2. 共享堆内存:shared_ptr
    auto sp = std::make_shared<std::vector<int>>(100, 7);
    std::shared_ptr<std::vector<int>> sp2 = sp;  // 引用计数 +1

    // 3. 弱引用:不参与计数,避免循环引用
    std::weak_ptr<std::vector<int>> wp = sp;

    // 4. 文件流:自动关闭
    std::ofstream out("log.txt");
    out << "hello\n";

    // 5. 互斥锁:作用域结束自动解锁
    std::mutex mtx;
    {
        std::lock_guard<std::mutex> lock(mtx);
        // 临界区...
    }

    // 6. 线程:C++20 jthread 自动 join
    std::jthread t([] {
        // 工作线程...
    });
    // 作用域结束自动 join
}

示例 3:自定义删除器接管 C API

#include <memory>
#include <cstdio>
#include <dlfcn.h>

/// 用 unique_ptr 接管 C 风格句柄:dlopen/dlclose
struct DlcloseDeleter {
    void operator()(void* handle) const noexcept {
        if (handle) ::dlclose(handle);
    }
};

using DlHandle = std::unique_ptr<void, DlcloseDeleter>;

DlHandle load_library(const char* path) {
    void* h = ::dlopen(path, RTLD_LAZY);
    if (!h) {
        throw std::runtime_error(std::string("dlopen: ") + ::dlerror());
    }
    return DlHandle{h};
}

/// 接管 FILE*:使用函数指针删除器
std::unique_ptr<FILE, decltype(&::fclose)> open_file(const char* path,
                                                       const char* mode) {
    FILE* f = ::fopen(path, mode);
    if (!f) {
        throw std::runtime_error("fopen failed");
    }
    return {f, &::fclose};
}

/// 接管 C 数组:自定义数组删除器
struct FreeDeleter {
    void operator()(void* p) const noexcept { std::free(p); }
};
template <typename T>
using CArray = std::unique_ptr<T, FreeDeleter>;

示例 4:完美转发与容器中的 RAII

#include <memory>
#include <vector>
#include <string>

class Widget {
public:
    Widget(int x, std::string s) : x_(x), s_(std::move(s)) {}
private:
    int x_;
    std::string s_;
};

void container_raii() {
    // vector 持有 Widget 对象的所有权(值语义 RAII)
    std::vector<Widget> v;
    v.reserve(100);
    for (int i = 0; i < 100; ++i) {
        v.emplace_back(i, std::string("name") + std::to_string(i));
    }
    // 离开作用域,vector 析构 -> 100 个 Widget 析构 -> string 析构
    // 所有资源链式释放

    // vector 持有 unique_ptr:移动语义必须显式
    std::vector<std::unique_ptr<Widget>> pv;
    pv.reserve(100);
    for (int i = 0; i < 100; ++i) {
        pv.push_back(std::make_unique<Widget>(i, "name"));
        // 或 pv.emplace_back(std::make_unique<Widget>(i, "name"));
    }
}

示例 5:异常安全的复制赋值(copy-and-swap 惯用法)

#include <algorithm>
#include <cstring>

class Buffer {
public:
    Buffer(size_t n) : data_(new char[n]()), size_(n) {}
    ~Buffer() { delete[] data_; }

    // 复制构造:深拷贝
    Buffer(const Buffer& other)
        : data_(new char[other.size_]), size_(other.size_) {
        std::memcpy(data_, other.data_, size_);
    }

    // copy-and-swap:先拷贝再交换,保证强异常安全
    Buffer& operator=(Buffer other) noexcept {  // 注意按值传参
        swap(other);
        return *this;
    }

    // 移动构造
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    void swap(Buffer& other) noexcept {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }

private:
    char* data_;
    size_t size_;
};

operator= 按值传参时,参数对象由复制构造函数创建(若实参为左值)或移动构造(若实参为右值)。任何异常都发生在参数构造阶段,此时 *this 尚未修改——天然强异常安全。

示例 6:RAII 管理锁的层次结构

#include <mutex>

class ThreadSafeCounter {
public:
    void increment() {
        std::scoped_lock lock(mtx_);  // C++17 多锁原子获取
        ++value_;
    }

    int get() const {
        std::scoped_lock lock(mtx_);
        return value_;
    }

    int increment_and_get() {
        std::scoped_lock lock(mtx_);
        ++value_;
        return value_;
    }

private:
    mutable std::mutex mtx_;
    int value_ = 0;
};

std::scoped_lock 在构造时获取一个或多个互斥量(使用死锁避免算法),析构时统一释放。即便 ++value_ 抛异常(实际上 int++ 不会),锁也会被释放。

对比分析

RAII vs 其他资源管理模式

维度RAII (C++)try-with-resources (Java)using (C#)with (Python)defer (Go)借用检查 (Rust)
强制性编译器强制析构编译器检查接口实现编译器检查接口运行时运行时编译期所有权分析
异常安全自动(栈展开)自动自动部分自动自动(panic 时)编译期保证
适用资源内存+非内存仅非内存(GC 管内存)仅非内存仅非内存任意内存+非内存
性能开销零(编译期内联)JIT 优化JIT 优化解释执行一次性开销
错误恢复析构不抛异常try-with-catch异常传播异常传播显式检查 errResult/Option
学习曲线中(需理解对象语义)极低高(生命周期)

智能指针横向对比

指针类型所有权复制移动开销适用场景
T* (裸指针)可(值拷贝)与 C 互操作;性能极端关键
std::unique_ptr<T>独占禁止转移零(同裸指针)默认堆对象所有权
std::shared_ptr<T>共享引用计数+1转移引用计数原子操作(线程安全)共享所有权;图节点
std::weak_ptr<T>弱引用引用计数不变转移原子操作打破循环引用;缓存
std::auto_ptr<T>(已弃用)独占(伪装复制)转移所有权不支持已弃用,勿用

RAII 类设计选择决策树

是否需要在多个对象间共享所有权?
├─ 是 → 使用 shared_ptr<T>,考虑是否需要 weak_ptr 打破循环
└─ 否 → 是否需要堆上分配?
    ├─ 否 → 使用栈对象 / 直接成员(首选)
    └─ 是 → 使用 unique_ptr<T>(默认)
          └─ 是否需要自定义删除器?
              ├─ 是 → unique_ptr<T, Deleter>
              └─ 否 → unique_ptr<T>

常见陷阱

陷阱 1:构造函数部分失败导致资源泄漏

反例

// 错误:构造函数中先 new 再 open,若 open 失败则内存泄漏
class Bad {
public:
    Bad(const char* path) {
        data_ = new char[1024];        // 获取内存
        fd_ = ::open(path, O_RDONLY);   // 若失败,data_ 泄漏
        if (fd_ < 0) {
            throw std::runtime_error("open failed");
            // data_ 未释放!
        }
    }
    ~Bad() { delete[] data_; ::close(fd_); }
private:
    char* data_;
    int fd_;
};

正例:使用 RAII 成员,让构造失败时自动释放已获取的资源。

class Good {
public:
    Good(const char* path)
        : data_(std::make_unique<std::array<char, 1024>>()),
          fd_(std::make_unique<FileDescriptor>(path, O_RDONLY)) {
        // data_ 已先构造,fd_ 构造抛异常时 data_ 自动析构释放
    }
private:
    std::unique_ptr<std::array<char, 1024>> data_;
    std::unique_ptr<FileDescriptor> fd_;
};

C++ 标准保证:构造函数抛异常时,已构造的成员与基类子对象按逆序析构。

陷阱 2:析构函数抛异常导致 std::terminate

class BadCloser {
public:
    ~BadCloser() {
        if (::close(fd_) < 0) {
            throw std::runtime_error("close failed");  // UB!
        }
    }
};

析构函数默认 noexcept,抛异常将触发 std::terminate。即使标 noexcept(false),在栈展开期间再次抛异常也会导致 terminate正确做法:析构函数中 try/catch 吞掉异常,或转记日志。

class GoodCloser {
public:
    ~GoodCloser() noexcept {
        try {
            if (::close(fd_) < 0) {
                std::cerr << "close failed: " << std::strerror(errno);
            }
        } catch (...) {
            // 不可让异常逃出析构
        }
    }
};

陷阱 3:std::auto_ptr 的复制陷阱(已弃用)

std::auto_ptr<int> p1(new int(42));
std::auto_ptr<int> p2 = p1;  // 看似复制,实则 p1 被置空!
std::cout << *p1;           // UB:解引用空指针

auto_ptr 的复制构造执行移动语义却伪装成复制,违反了复制语义的可预期性。C++17 弃用,C++26 移除。永远使用 unique_ptr

陷阱 4:shared_ptr 循环引用

struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // 双向链表:循环引用!
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // 引用计数循环:a, b 永不释放

修复:将其中一条边改为 weak_ptr

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // 弱引用,不参与计数
};

// 使用时 lock() 提升为 shared_ptr
auto prev = node->prev.lock();
if (prev) { /* 安全使用 prev */ }

陷阱 5:移动后使用源对象

auto p = std::make_unique<int>(42);
auto q = std::move(p);
std::cout << *p;  // UB:p 已被移动,处于有效但未指定状态

标准只保证移动后的源对象”有效但未指定”(valid but unspecified),不保证其为 nullptr。对 unique_ptr 而言实际上是 nullptr,但不应依赖此细节。

陷阱 6:RAII 与虚函数的构造/析构陷阱

class Base {
public:
    Base() { init(); }            // 虚调用!实际调用 Base::init
    virtual void init() { std::cout << "Base\n"; }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void init() override { std::cout << "Derived\n"; }
};

Derived d;  // 输出 "Base" 而非 "Derived"

构造/析构期间虚函数按静态类型解析。若在 RAII 构造中依赖派生类虚函数完成资源初始化,将产生意外行为。解决方案:构造函数中不调用虚函数;若必须,则改用两阶段构造(factory 模式)。

陷阱 7:跨 DLL 边界传递 RAII 对象

// DLL A 编译
std::shared_ptr<int> sp = std::make_shared<int>(42);
// 传递给 DLL B(不同编译器/版本)
pass_to_dll_b(sp);  // 引用计数操作可能跨 ABI 不兼容

shared_ptr 的引用计数使用类型擦除,其删除器与分配器可能跨 DLL 边界不兼容。最佳实践:跨 DLL 边界仅传递裸指针 + 显式所有权约定,或使用 C 接口。

陷阱 8:在析构函数中调用 delete this

class Evil {
public:
    void suicide() { delete this; }
    ~Evil() { /* 此时 this 已被销毁,访问任何成员皆 UB */ }
};

delete this 必须保证:对象由 new 创建,调用后不再访问任何成员。这是危险设计,仅在引用计数式对象(如 COM)中使用

工程实践

实践 1:以 RAII 为默认资源策略

在团队编码规范中明确:

  • 默认:所有资源(堆内存、文件、锁、socket、GPU buffer)必须由 RAII 类型持有。
  • 例外:仅在与 C API 交互的薄包装层允许裸指针,且其生命周期被 RAII 严格限制。
  • 静态检查:CI 集成 clang-tidy 规则 cppcoreguidelines-owning-memorymodernize-use-nullptrmodernize-make-uniquebugprone-use-after-move

实践 2:编译期断言 RAII 性质

使用 static_assert 验证类型符合 RAII 语义:

#include <type_traits>

template <typename T>
constexpr bool is_raii_type_v =
    std::is_destructible_v<T> &&
    std::is_nothrow_destructible_v<T>;

static_assert(is_raii_type_v<std::unique_ptr<int>>);
static_assert(is_raii_type_v<std::vector<int>>);
static_assert(is_raii_type_v<std::string>);

进一步,使用 C++20 概念约束 RAII 类型:

template <typename T>
concept RAII = requires(T t) {
    { T(std::move(t)) } noexcept;  // 可 noexcept 移动
    { ~T() } noexcept;             // 可 noexcept 析构
};

template <RAII T>
class ResourceManager {
    T resource_;
};

实践 3:异常安全等级标注

在 API 文档中显式标注异常安全等级:

/// @brief 提交事务
/// @throws std::runtime_error 若数据库不可达
/// @exception_safety strong: 失败时事务状态不变
void commit();

实践 4:使用 [[nodiscard]] 防止资源泄漏

[[nodiscard]] std::unique_ptr<Connection> connect(const std::string& url);

若调用方忽略返回值,编译器警告。这避免了”获取了 RAII 句柄却立即丢弃导致资源立即释放”的陷阱。

实践 5:自定义删除器的策略选型

// 策略 1:无状态删除器(零开销,推荐)
struct FileCloser {
    void operator()(FILE* f) const noexcept { if (f) std::fclose(f); }
};
using FilePtr = std::unique_ptr<FILE, FileCloser>;
// sizeof(FilePtr) == sizeof(FILE*)

// 策略 2:函数指针删除器(增加一个指针大小)
using FilePtr2 = std::unique_ptr<FILE, decltype(&std::fclose)>;
// sizeof(FilePtr2) == 2 * sizeof(FILE*)

// 策略 3:有状态删除器(如带路径的关闭器,便于日志)
struct LoggedFileCloser {
    std::string path;
    void operator()(FILE* f) const noexcept {
        if (f) {
            std::fclose(f);
            log_close(path);
        }
    }
};
// sizeof(LoggedFileCloser) = sizeof(std::string)

删除器选择影响 unique_ptr 的大小。无状态删除器(空类)经空基类优化(EBO)后零开销。

实践 6:RAII 与 pimpl 惯用法

// widget.h
class Widget {
public:
    Widget();
    ~Widget();  // 必须声明(实现见 cpp)
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
private:
    struct Impl;
    std::unique_ptr<Impl> pimpl_;
};

// widget.cpp
#include "widget.h"
#include <vector>
#include <string>

struct Widget::Impl {
    std::vector<int> data;
    std::string name;
};

Widget::Widget() : pimpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // 此时 Impl 完整类型可见
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

pimpl 既是 ABI 解耦技术,也是 RAII 的应用——unique_ptr<Impl> 管理实现结构体的生命周期。注意头文件中必须声明析构函数(不能 = default),否则编译器在头文件中看到不完整类型 Impl 的析构会报错。

案例研究

案例 1:线程安全的多资源事务

考虑一个银行转账系统:从一个账户扣款、向另一账户存款、记录日志。三个资源(数据库连接、文件日志、互斥锁)必须协同工作,且保证强异常安全(commit-or-rollback)。

#include <memory>
#include <mutex>
#include <fstream>
#include <stdexcept>

class Account {
public:
    void deposit(int amount) {
        std::scoped_lock lock(mtx_);
        balance_ += amount;
    }
    void withdraw(int amount) {
        std::scoped_lock lock(mtx_);
        if (balance_ < amount) throw std::runtime_error("insufficient");
        balance_ -= amount;
    }
    int balance() const {
        std::scoped_lock lock(mtx_);
        return balance_;
    }
private:
    mutable std::mutex mtx_;
    int balance_ = 0;
};

class Logger {
public:
    explicit Logger(const char* path) : file_(path) {
        if (!file_) throw std::runtime_error("logger open failed");
    }
    void log(const std::string& msg) {
        file_ << msg << '\n';
        file_.flush();
    }
private:
    std::ofstream file_;
};

/// 转账:所有资源 RAII 持有,自动异常安全
void transfer(Account& from, Account& to, int amount, Logger& log) {
    // RAII 锁:保证解锁
    std::scoped_lock lock(from.mtx_, to.mtx_);
    // 步骤 1:扣款(若失败则抛异常,锁释放,状态不变)
    from.withdraw(amount);  // 内部再加锁?死锁风险!
    // 实际应改为直接操作 balance_,或重构 API
    // ... 假设此为简化示例 ...
    to.deposit(amount);
    log.log("transferred " + std::to_string(amount));
}

注意上述代码存在”再加锁死锁”问题(std::mutex 不可重入)。正确实现需要重构 API 暴露 unsafe_deposit/unsafe_withdraw 或使用 std::recursive_mutex。这个案例展示了 RAII 简化资源管理的同时,仍需开发者注意业务层面的并发设计。

案例 2:CUDA 资源管理

#include <cuda_runtime.h>
#include <memory>
#include <stdexcept>

struct CudaFreeDeleter {
    void operator()(void* p) const noexcept {
        ::cudaFree(p);
    }
};

template <typename T>
class CudaBuffer {
public:
    explicit CudaBuffer(size_t n) : size_(n) {
        T* raw = nullptr;
        cudaError_t err = ::cudaMalloc(&raw, n * sizeof(T));
        if (err != cudaSuccess) {
            throw std::runtime_error(::cudaGetErrorString(err));
        }
        ptr_.reset(raw);
    }

    T* get() noexcept { return ptr_.get(); }
    const T* get() const noexcept { return ptr_.get(); }
    size_t size() const noexcept { return size_; }

    void copy_from_host(const T* host) {
        cudaError_t err = ::cudaMemcpy(ptr_.get(), host,
                                        size_ * sizeof(T),
                                        cudaMemcpyHostToDevice);
        if (err != cudaSuccess) {
            throw std::runtime_error(::cudaGetErrorString(err));
        }
    }

private:
    std::unique_ptr<T, CudaFreeDeleter> ptr_;
    size_t size_;
};

// 使用:
void kernel_wrapper() {
    CudaBuffer<float> buf(1024);
    std::vector<float> host(1024, 1.0f);
    buf.copy_from_host(host.data());
    // ... 调用 kernel(buf.get()) ...
    // 离开作用域自动 cudaFree
}

案例 3:RAII 实现 scope guard(C++17 std::experimental::scope_guard 风格)

#include <utility>
#include <type_traits>

template <typename F>
class ScopeGuard {
public:
    explicit ScopeGuard(F&& f) : f_(std::move(f)), active_(true) {}
    ~ScopeGuard() { if (active_) f_(); }
    void release() noexcept { active_ = false; }

    ScopeGuard(const ScopeGuard&) = delete;
    ScopeGuard& operator=(const ScopeGuard&) = delete;
    ScopeGuard(ScopeGuard&& other) noexcept
        : f_(std::move(other.f_)), active_(other.active_) {
        other.active_ = false;
    }
private:
    F f_;
    bool active_;
};

template <typename F>
ScopeGuard<std::decay_t<F>> make_scope_guard(F&& f) {
    return ScopeGuard<std::decay_t<F>>(std::forward<F>(f));
}

// 使用:
#include <iostream>
void demo() {
    auto g = make_scope_guard([] { std::cout << "exit\n"; });
    // ... 执行工作 ...
    if (some_error) return;  // 输出 "exit"
    g.release();             // 取消,不输出
}

注意 scope_guard 中的 f_() 可能抛异常,违反析构 noexcept。生产级实现需要 try/catch 包裹。

案例 4:完整 RAII 事务日志分析

设计一个数据库连接池,要求:

  • 连接借用与归还通过 RAII 句柄自动管理;
  • 借用超时抛异常;
  • 归还时自动健康检查,失败则丢弃;
#include <memory>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <chrono>

class Connection {
public:
    bool is_healthy() const { return true; }
    void execute(const std::string& sql) { /* ... */ }
};

class ConnectionPool {
public:
    explicit ConnectionPool(size_t n) {
        for (size_t i = 0; i < n; ++i) {
            pool_.push_back(std::make_unique<Connection>());
        }
    }

    /// 借用句柄:RAII 管理归还
    class BorrowedConnection {
    public:
        BorrowedConnection(ConnectionPool* pool, Connection* conn,
                           std::unique_lock<std::mutex> lock)
            : pool_(pool), conn_(conn), lock_(std::move(lock)) {}

        ~BorrowedConnection() {
            if (pool_) {
                pool_->return_connection(conn_, !reclaimed_);
            }
        }

        BorrowedConnection(BorrowedConnection&&) = default;
        BorrowedConnection& operator=(BorrowedConnection&&) = default;

        Connection* operator->() noexcept { return conn_; }
        Connection& operator*() noexcept { return *conn_; }

    private:
        ConnectionPool* pool_;
        Connection* conn_;
        std::unique_lock<std::mutex> lock_;
        bool reclaimed_ = false;  // 是否已归还(防止双重释放)
    };

    BorrowedConnection acquire(std::chrono::milliseconds timeout) {
        std::unique_lock lock(mtx_);
        if (!cv_.wait_for(lock, timeout, [this] { return !pool_.empty(); })) {
            throw std::runtime_error("acquire timeout");
        }
        Connection* raw = pool_.back().release();  // 转移所有权
        pool_.pop_back();
        return BorrowedConnection(this, raw, std::move(lock));
    }

private:
    void return_connection(Connection* conn, bool reuse) {
        if (reuse && conn->is_healthy()) {
            pool_.push_back(std::unique_ptr<Connection>(conn));
        } else {
            delete conn;  // 不健康则销毁
        }
        cv_.notify_one();
    }

    std::vector<std::unique_ptr<Connection>> pool_;
    std::mutex mtx_;
    std::condition_variable cv_;
};

// 使用:
void use_pool(ConnectionPool& pool) {
    auto conn = pool.acquire(std::chrono::seconds(5));
    conn->execute("SELECT 1");
    // 离开作用域自动归还连接
}

案例 5:自定义 scoped_resource 通用模板

#include <utility>
#include <type_traits>

/// 通用 RAII 资源句柄:支持任意资源类型与删除器
template <typename T, typename Deleter>
class scoped_resource {
public:
    /// 资源 + 删除器构造
    template <typename U, typename D>
    scoped_resource(U&& resource, D&& deleter)
        : resource_(std::forward<U>(resource)),
          deleter_(std::forward<D>(deleter)),
          active_(true) {}

    ~scoped_resource() noexcept {
        if (active_) {
            try { deleter_(resource_); } catch (...) { /* swallow */ }
        }
    }

    scoped_resource(scoped_resource&& other) noexcept
        : resource_(std::move(other.resource_)),
          deleter_(std::move(other.deleter_)),
          active_(other.active_) {
        other.active_ = false;
    }

    scoped_resource& operator=(scoped_resource&& other) noexcept {
        if (this != &other) {
            if (active_) {
                try { deleter_(resource_); } catch (...) {}
            }
            resource_ = std::move(other.resource_);
            deleter_ = std::move(other.deleter_);
            active_ = other.active_;
            other.active_ = false;
        }
        return *this;
    }

    scoped_resource(const scoped_resource&) = delete;
    scoped_resource& operator=(const scoped_resource&) = delete;

    void release() noexcept { active_ = false; }
    T& get() noexcept { return resource_; }
    const T& get() const noexcept { return resource_; }

private:
    T resource_;
    Deleter deleter_;
    bool active_;
};

// 使用:
auto fd = scoped_resource<int, void(*)(int)>(
    ::open("file.txt", O_RDONLY),
    [](int fd) { if (fd >= 0) ::close(fd); });

性能分析

RAII 的运行时开销

操作开销备注
RAII 对象构造(栈)资源获取成本与手写相同
RAII 对象析构(栈)资源释放成本与手写相同
unique_ptr<T> 复制编译期错误delete 复制构造
unique_ptr<T> 移动1 条 mov 指令零开销
shared_ptr<T> 复制原子 fetch_add(约 20-50 ns)缓存行影响
shared_ptr<T> 移动非原子赋值(约 1-2 ns)比复制快
weak_ptr 提升为 shared_ptr原子 CAS(约 30-60 ns)可能失败

ABI 与代码体积

std::unique_ptr<T>T* 的 ABI 完全一致(sizeof 相同,寄存器传递相同)。shared_ptr<T> 占两个指针(对象指针 + 控制块指针),ABI 上与 std::pair<T*, control_block*> 一致。这些设计使 RAII 类型能与 C ABI 无缝互操作。

实测对比:RAII vs 手写

// 手写 C 风格
void manual() {
    int* p = (int*)malloc(sizeof(int));
    if (!p) return;
    *p = 42;
    printf("%d\n", *p);
    free(p);
}

// RAII
void raii() {
    auto p = std::make_unique<int>(42);
    printf("%d\n", *p);
}

GCC 13.2 -O3 生成汇编完全一致(详见推导 4 的反汇编)。RAII 不引入任何运行时开销。

与 C++26 新特性的协同

静态反射(P2996)

C++26 草案中的静态反射将允许在编译期枚举 RAII 类的成员、识别资源类型、自动生成序列化代码:

// C++26 草案(语法可能调整)
#include <meta>

template <typename T>
void describe_raii() {
    constexpr auto info = ^^T;
    std::cout << "Type: " << name_of(info) << "\n";
    std::cout << "Members:\n";
    for (auto member : nonstatic_data_members_of(info)) {
        std::cout << "  " << name_of(member) << ": "
                  << name_of(type_of(member)) << "\n";
    }
}

这将使通用反射式序列化框架自动识别 RAII 类的资源语义。

契约编程(P2900)

契约编程将允许在 RAII 类的不变式上添加运行期断言:

class FileDescriptor {
public:
    [[pre: path != nullptr]]
    explicit FileDescriptor(const char* path, int flags);

    [[invariant: fd_ >= -1]]
    ~FileDescriptor();
};

std::expected 与错误传播

C++23 引入的 std::expected<T, E> 提供值 RAII 的错误传播通道,可与 RAII 资源管理协同:

std::expected<FileDescriptor, std::error_code> open_checked(const char* path) {
    int fd = ::open(path, O_RDONLY);
    if (fd < 0) return std::unexpected(std::error_code(errno, std::system_category()));
    return FileDescriptor(fd);  // 假设有接管构造
}

与现代 C++ 特性的整合

移动语义(C++11)

RAII 与移动语义的协同使资源可以廉价转移:

std::vector<std::unique_ptr<Widget>> make_widgets() {
    std::vector<std::unique_ptr<Widget>> v;
    v.reserve(100);
    for (int i = 0; i < 100; ++i) {
        v.push_back(std::make_unique<Widget>(i));
    }
    return v;  // 移动语义:O(1) 转移所有权,非 O(n) 拷贝
}

完美转发(C++11)

template <typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

constexpr 与编译期 RAII(C++20)

constexpr int compute() {
    std::array<int, 10> arr{};  // RAII 数组
    for (int i = 0; i < 10; ++i) arr[i] = i * i;
    int sum = 0;
    for (auto x : arr) sum += x;
    return sum;  // arr 在编译期"析构"(无操作)
}
static_assert(compute() == 285);

C++20 起允许 std::vectorstd::stringconstexpr 上下文中使用,使 RAII 容器可在编译期工作。

概念约束(C++20)

template <typename T>
concept Resource = requires(T t) {
    { t.release() } -> std::same_as<void>;
    { t.get() };
};

template <Resource R>
class ResourceManager {
    R resource_;
};

设计哲学:RAII 与 C++ 的本质

RAII 不仅是技术手段,更是 C++ 的世界观:

  1. 类型即资源:每个类型对应一类资源,类型系统是资源管理的最佳载体。
  2. 作用域即生命周期:栈对象的作用域天然定义了资源的生命周期边界。
  3. 确定性析构:与 GC 不同,C++ 的析构是确定性的——程序员知道资源何时释放。
  4. 零开销抽象:高级抽象不带来运行时开销,这是 C++ 区别于 Java/C# 的根本。

Stroustrup 在多个场合强调:

“C++‘s best feature is its deterministic resource management. … RAII is the foundation of every modern C++ library.”(C++ 最优秀的特性是其确定性资源管理。RAII 是每个现代 C++ 库的基础。)

理解 RAII 不只是掌握一个语法特性,更是理解 C++ 区别于其他语言的哲学根基。

习题

基础题

Q1:以下哪个不是 RAII 的核心要素?

A. 构造函数获取资源 B. 析构函数释放资源 C. 拷贝构造深拷贝资源 D. 资源所有权绑定到对象生命周期

Q2:下面代码有什么问题?如何修复?

class FileWrapper {
public:
    FileWrapper(const char* path) : f_(std::fopen(path, "r")) {}
    ~FileWrapper() { std::fclose(f_); }
private:
    FILE* f_;
};

Q3:使用 unique_ptr 与自定义删除器包装 OpenGL 的 GLuint 纹理对象(生成用 glGenTextures,删除用 glDeleteTextures)。

Q4:解释为什么 std::auto_ptr 被弃用,它违反了哪条 RAII 不变式?

Q5:写出以下类型的异常安全等级(基本/强/不抛):

  • std::vector<int>::push_back
  • std::unique_ptr<int>::~unique_ptr
  • std::shared_ptr<int> 复制构造
  • std::mutex::lock

进阶题

Q6:设计一个 scoped_thread 类,包装 std::thread,要求:

  • 析构时若线程仍 joinable(),则 join()
  • 不可复制,可移动;
  • 移动后源对象处于”已 join”状态。

Q7:实现 scope_fail(仅在异常抛出时执行)与 scope_success(仅在正常退出时执行)。提示:检查析构时的 std::uncaught_exceptions() 数量。

Q8:分析以下代码,指出所有 RAII 违规之处并修复:

class ConnectionPool {
public:
    Connection* acquire() {
        if (pool_.empty()) return nullptr;
        Connection* c = pool_.back();
        pool_.pop_back();
        return c;  // 返回裸指针
    }
    void release(Connection* c) {
        pool_.push_back(c);
    }
private:
    std::vector<Connection*> pool_;
};

Q9:使用 shared_ptr 实现一个简单观察者模式,要求订阅者持有发布者的 weak_ptr,避免循环引用。

Q10:分析 std::shared_ptr 在多线程下引用计数的实现机制,解释为什么需要原子操作。

挑战题

Q11:实现一个通用的 transaction 模板,接受多个 RAII 操作,保证全部成功或全部回滚:

auto t = make_transaction(
    [&] { account.withdraw(100); },
    [&] { account.deposit(100); },
    [&] { log.write("transfer"); }
);
t.commit();  // 三个操作要么全做,要么全不做

提示:使用 std::exception_ptr 记录失败操作,回滚时调用对应”反操作”。

Q12:在 CUDA 场景下设计一个 pinned_memory RAII 类,要求:

  • cudaMallocHost 分配,cudaFreeHost 释放;
  • 支持移动语义;
  • 提供 data()size() 接口;
  • 通过 is_raii_type_v 静态检查。

Q13:实现一个 scope_exitscope_fail 的统一模板,仅当 bool 模板参数为 true 时执行回滚。讨论其在编译器优化下的零开销证明。

Q14:分析 std::shared_ptr 的控制块(control block)内存布局,解释为什么 make_shared 通常比 shared_ptr(new T) 更高效,但 Dtor 时机可能更晚。给出具体场景权衡。

Q15:设计一个编译期 RAII 链,要求:在 constexpr 函数中使用 std::arraystd::string_view,模拟一个编译期字符串处理流水线,并保证不分配堆内存。

参考答案要点

A1:C。RAII 不要求拷贝构造深拷贝——std::unique_ptr 不可复制,std::mutex 也不可复制。RAII 的核心是”资源绑定到对象生命周期”。

A2:(1) 若 fopen 返回 NULL,析构时 fclose(nullptr) 是 UB;(2) 类不可复制,否则双重 fclose;(3) 缺少移动语义。修复:构造失败抛异常、delete 复制构造、实现移动构造。

A3

struct GlTextureDeleter {
    void operator()(GLuint* p) const noexcept {
        ::glDeleteTextures(1, p);
        delete p;
    }
};
using GlTexture = std::unique_ptr<GLuint, GlTextureDeleter>;

GlTexture make_texture() {
    auto p = std::make_unique<GLuint>();
    ::glGenTextures(1, p.get());
    return GlTexture(p.release());
}

A4auto_ptr 复制构造执行移动语义却伪装成复制,违反”复制后源对象与副本独立”的不变式 I4。

A5

  • push_back:强保证(必要时抛 bad_alloc,向量不变);
  • ~unique_ptr:不抛;
  • shared_ptr 复制:不抛(原子操作);
  • mutex::lock:基本保证(若失败抛 system_error,互斥量未锁定)。

A6

class scoped_thread {
public:
    explicit scoped_thread(std::thread t) : t_(std::move(t)) {
        if (!t_.joinable()) throw std::logic_error("no thread");
    }
    ~scoped_thread() { if (t_.joinable()) t_.join(); }
    scoped_thread(const scoped_thread&) = delete;
    scoped_thread& operator=(const scoped_thread&) = delete;
    scoped_thread(scoped_thread&&) = default;
    scoped_thread& operator=(scoped_thread&&) = default;
private:
    std::thread t_;
};

A7

template <typename F>
class scope_fail {
public:
    explicit scope_fail(F&& f) : f_(std::move(f)), count_(std::uncaught_exceptions()) {}
    ~scope_fail() {
        if (std::uncaught_exceptions() > count_) {
            try { f_(); } catch (...) {}
        }
    }
    scope_fail(const scope_fail&) = delete;
    scope_fail& operator=(const scope_fail&) = delete;
private:
    F f_;
    int count_;
};

scope_success 反之:仅在 uncaught_exceptions() == count_ 时执行。

A8:(1) 返回裸指针,调用方可能忘记 release;(2) pool_ 持有裸指针,无所有权;(3) 无线程安全;(4) 无异常安全。修复:返回 unique_ptr<Connection, ...> 借用句柄。

A9:略,参见案例 4。

A10shared_ptr 的引用计数需要被多个线程同时访问(构造、析构、复制)。原子操作保证计数自增/自减是原子的,避免数据竞争。但注意:引用计数原子,但指向对象本身的多线程访问仍需同步

A11-A15:参见延伸阅读与开源实现(如 std::experimental::scope_exit、Folly RAII 工具)。

参考文献

[1] Stroustrup, B. 1994. The Design and Evolution of C++. Addison-Wesley Professional, Reading, MA. ISBN: 0-201-54330-3.

[2] Stroustrup, B. and Abrahams, D. 2002. Exception-Safety in Generic Components. In Generic Programming: Proceedings of the International Seminar on Generic Programming (Dagstuhl Castle, Germany, 1998). Lecture Notes in Computer Science, vol. 1766. Springer, Berlin, 68–79. DOI: 10.1007/3-540-39953-4_6.

[3] Abrahams, D. 1998. Exception-Safety in Generic Components. In Proceedings of the 1st Workshop on C++ Template Programming (Manchester, UK). Available at: https://www.boost.org/community/exception_safety.html.

[4] International Organization for Standardization. 2020. Information technology — Programming languages — C++. ISO/IEC 14882:2020. ISO, Geneva, Switzerland.

[5] International Organization for Standardization. 2023. Information technology — Programming languages — C++. ISO/IEC 14882:2023. ISO, Geneva, Switzerland.

[6] Meyers, S. 2005. Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd ed.). Addison-Wesley Professional, Boston, MA. ISBN: 0321334876.

[7] Sutter, H. and Alexandrescu, A. 2004. C++ Coding Standards: 101 Rules, Guidelines, and Best Practices. Addison-Wesley Professional, Boston, MA. ISBN: 0321113586.

[8] Sutter, H. 1999. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions. Addison-Wesley Professional, Boston, MA. ISBN: 0201615622.

[9] Alexandrescu, A. 2001. Modern C++ Design: Generic Programming and Design Patterns Applied. Addison-Wesley Professional, Boston, MA. ISBN: 0201704315.

[10] Williams, A. 2019. C++ Concurrency in Action (2nd ed.). Manning Publications, Shelter Island, NY. ISBN: 1617294691.

[11] Stroustrup, B. 2013. The C++ Programming Language (4th ed.). Addison-Wesley Professional, Boston, MA. ISBN: 0321563840.

[12] Stroustrup, B. 2022. A Tour of C++ (3rd ed.). Addison-Wesley Professional, Boston, MA. ISBN: 0136816487.

[13] Josuttis, N. M. 2021. C++20: The Complete Guide. Self-published. ISBN: 9783967300104.

[14] ISO/IEC JTC1/SC22/WG21. 2024. Working Draft, C++26. N4988. Available at: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/n4988.pdf.

[15] Sutton, A. and Sankel, D. 2024. Reflection for C++26 (P2996R5). ISO/IEC JTC1/SC22/WG21. Available at: https://wg21.link/p2996r5.

[16] Gregor, D., Stroustrup, B., et al. 2003. Proposed Wording for Implicitly-Callable Move Operations (N1773). ISO/IEC JTC1/SC22/WG21.

[17] Becker, P. 2011. Working Draft, Standard for Programming Language C++ (N3242). ISO/IEC JTC1/SC22/WG21.

[18] Halpern, P. 2024. Contracts for C++ (P2900R9). ISO/IEC JTC1/SC22/WG21. Available at: https://wg21.link/p2900r9.

[19] Milewski, B. 2018–2024. Category Theory for Programmers. Available at: https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/.

[20] Jung, A. and Turo, D. 1995. A Cook’s Tour of the Generic C++ Standard Template Library. ACM SIGPLAN Notices 30, 6, 28–41. DOI: 10.1145/211442.211448.

延伸阅读

  • Boost.Coreboost::core::noncopyableboost::scope_exitboost::intrusive_ptr 等通用 RAII 工具。
  • Folly:Facebook 开源库中的 folly::RAIIfolly::ScopeGuardfolly::SemiFuture 等异步 RAII 设施。
  • Abseil:Google 开源库中的 absl::Cleanupabsl::StatusOr 等现代 RAII 模式。
  • tl::expected:Sy Brand 的 std::expected 参考实现,包含 RAII 错误传播示例。
  • GSL (Guideline Support Library):C++ Core Guidelines 的参考实现,gsl::owner<T*>gsl::finally
  • Clang Static Analyzeralpha.cplusplus.UninitializedObjectalpha.security.taint 等检查器可检测 RAII 违规。
  • clang-tidy rulescppcoreguidelines-*modernize-*bugprone-* 系列规则大量覆盖 RAII 模式。
  • Sanitizers:AddressSanitizer (ASan)、LeakSanitizer (LSan)、ThreadSanitizer (TSan) 可在运行期检测 RAII 失效导致的内存错误。
  • 论文:Wilson, M. 2003. Imperfect C++: Practical Solutions for Real-Life Programming. Addison-Wesley. 深入讨论 RAII 的局限与折衷。
  • 教学资源:MIT 6.172 Performance Engineering of Software Systems 课程中关于 C++ 内存管理的章节,CMU 15-411 Compiler Design 中关于对象生命周期的部分。
  • 官方文档:cppreference.com 上的 “RAII” 条目与 C++ Standard Library 模块文档,提供完整的标准库 RAII 设施索引。
  • 未来方向:关注 C++26 标准中的反射(P2996)、契约(P2900)以及模式匹配(P2688)提案,它们将进一步扩展 RAII 的表达力。
返回入门指南