专栏 知识宝典 子专栏 编程语言精进 23 篇

1.4.2 C++ RAII 与移动语义

RAII + 移动语义是 C++ 资源管理的灵魂。

1. RAII 原则:析构自动释放

RAII(Resource Acquisition Is Initialization)把资源的生命周期与对象的 scope 绑定:在构造函数里获取资源,在析构函数里释放资源。只要对象离开作用域——无论是正常 return、break、continue,还是抛异常触发栈展开——C++ 保证其析构函数一定运行。这是 C++ 区别于 C/Java/Go 的根本特征:你不需要 defer、不需要 try/finally,语言本身用 destructor 的确定性调用替你擦屁股。掌握这条原则后,内存、文件句柄、互斥锁、CUDA stream 都能用同一种范式管。

class FileGuard {
public:
    explicit FileGuard(const char* path) : fp_(std::fopen(path, "r")) {
        if (!fp_) throw std::runtime_error("open failed");
    }
    ~FileGuard() { if (fp_) std::fclose(fp_); }   // 异常路径也会执行
    FILE* get() const { return fp_; }
private:
    FILE* fp_;
};
资源类型 RAII 包装做法 释放点
堆内存 new 在 ctor,delete 在 dtor 作用域结束 / 异常栈展开
文件 / socket ctor 打开,dtor 关闭 同上
mutex ctor lock,dtor unlock 同上
CUDA device mem ctor cudaMalloc,dtor cudaFree 同上
TensorRT engine ctor builder->build(),dtor engine->destroy() 同上

调研依据:ISO C++ Core Guidelines E.6(Esc.6) 与 R.1 明确推荐 RAII 作为所有资源管理的唯一手段;Bjarne Stroustrup 在《The C++ Programming Language》4th 中将 RAII 列为 C++ 区别于其他语言最关键的设计。

2. 移动语义:std::move / 右值引用 / std::forward

C++11 引入右值引用 T&& 与 std::move,把”复制”变成”转移”:资源指针从源对象偷走,源对象置空,目标对象独占,零拷贝也不留隐患。std::forward 配合模板与引用折叠规则实现完美转发,把左值/右值属性无损地传到内层函数。三件套合起来,让容器 std::vector<std::string> 扩容、std::unique_ptr 转移、std::thread 移交执行权都变成常数时间。

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

    Buffer(const Buffer&) = delete;                            // 禁拷贝
    Buffer& operator=(const Buffer&) = delete;

    Buffer(Buffer&& other) noexcept                            // 移动构造
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr; other.size_ = 0;
    }
    Buffer& operator=(Buffer&& other) noexcept { /* 同上 */ return *this; }
private:
    int* data_; size_t size_;
};

template <class T>
void relay(T&& x) { sink(std::forward<T>(x)); }               // 完美转发
概念 写法 作用
左值引用 T& 绑定有名字的对象,可读写
右值引用 T&& 绑定临时对象 / std::move 结果,可”偷”
std::move static_cast<T&&>(lvalue) 把左值标成右值,触发移动
std::forward 保留实参的值类别 模板完美转发
移动构造/赋值 参数 T&&,noexcept 标注 O(1) 资源转移

调研依据:ISO/IEC 14882:2011 引入 rvalue reference([dcl.ref]) 与 move semantics([class.move]);Howard Hinnant《Rvalue References: Moving Forward》(2014) 与 P0135R1 把 noexcept 标在移动构造上的意义讲透——std::vector 扩容时只有 noexcept_move_constructible 才走移动而非复制。

3. 智能指针选型表:unique_ptr / shared_ptr / weak_ptr

<memory> 头文件提供三种所有权模型:unique_ptr 独占 + 零开销;shared_ptr 共享 + 引用计数(原子加减);weak_ptr 不计数 + 观察者,用于打破循环引用。选型不是”哪个新用哪个”,而是匹配所有权语义:90% 的场景 unique_ptr 就够,多所有者才上 shared_ptr,需要观察但不延长生命周期则用 weak_ptr。

auto p = std::make_unique<Tensor>("input", shape);     // 独占
auto q = std::make_shared<Engine>(config);             // 共享
std::weak_ptr<Engine> obs = q;                         // 不延长
if (auto alive = obs.lock()) alive->infer(x);
智能指针 所有权 额外开销 典型场景 注意事项
unique_ptr<T> 独占(可 move,不可 copy) 0,等同裸指针 工厂返回值、Pimpl、CUDA wrapper 不要隐式转 shared_ptr,会反生
shared_ptr<T> 共享(引用计数) 引用计数原子操作(2× atomic),2× 内存 多所有者、跨线程共享资源 循环引用会泄漏,需 weak_ptr 破环
weak_ptr<T> 观察,不计 一次原子加 + lock 检查 缓存、订阅者、打破循环 必须 .lock() 后再使用

调研依据:CppCon 2019 Arthur O’Dwyer《Back to Basics: Smart Pointers》给出定量化对比:unique_ptr 与裸指针同 size 同性能;shared_ptr 比 unique_ptr 慢 5-30×(取决于多线程竞争),make_shared 比 new+ctor 少一次分配,推荐默认使用。

4. TensorRT C++ 接口 RAII 模式

NVIDIA TensorRT 的 C API/IBuilder/IExecutionContext 是 C++ 接口,但内部用裸指针管 GPU 资源——destroy() 必须显式调用,中途抛异常就泄漏。生产代码一律用 RAII wrapper 包一层:构造时建对象,析构时 destroy(),内部再封装 unique_ptr 加自定义 deleter,把 TensorRT 的所有权语义映射到现代 C++。

struct TrtEngineDeleter { void operator()(nvinfer1::ICudaEngine* e) const noexcept { if (e) e->destroy(); } };
struct TrtContextDeleter { void operator()(nvinfer1::IExecutionContext* c) const noexcept { if (c) c->destroy(); } };

using TrtEnginePtr  = std::unique_ptr<nvinfer1::ICudaEngine, TrtEngineDeleter>;
using TrtContextPtr = std::unique_ptr<nvinfer1::IExecutionContext, TrtContextDeleter>;

class InferSession {
public:
    InferSession(TrtEnginePtr eng) : engine_(std::move(eng)) {
        ctx_.reset(engine_->createExecutionContext());   // 失败自动 rollback
    }
private:
    TrtEnginePtr engine_;
    TrtContextPtr ctx_;
};
TensorRT 对象 C 接口语义 RAII 包装方案 销毁函数
IRuntime 应用级单例 unique_ptr + deleter runtime->destroy()
ICudaEngine 序列化产物 unique_ptr + deleter engine->destroy()
IExecutionContext 推理会话 unique_ptr + deleter context->destroy()
cudaStream_t 异步队列 unique_ptr + lambda deleter cudaStreamDestroy
cudaEvent_t 同步事件 unique_ptr + lambda deleter cudaEventDestroy
void* device buffer GPU 显存 unique_ptr<void, decltype(&cudaFree)> cudaFree

调研依据:NVIDIA TensorRT 8.6/10.x Developer Guide §”Creating a Network Definition” 与 sampleOnnxMNIST.cpp 源码中 samplesCommon::InferDeleter 自定义 deleter 模式;TensorRT 11 起要求 createExecutionContext 失败的 engine 自动回滚——RAII 的对象所有权正好覆盖这一约束。

5. 实战:RAII 管理 GPU 显存

一个真实推理管线要在同一个进程里管理多份 GPU buffer:输入 tensor、输出 tensor、KV cache、workspace。裸 cudaMalloc/cudaFree 容易在异常路径泄漏;shared_ptr<void> 加自定义 deleter 能把显存也纳入智能指针体系,优雅地支持多所有者(输入/输出共享同一块 buffer)与循环引用规避(网络持有观察)。配合 cudaStream_t 的 RAII wrapper,异步推理的整个生命周期只用作用域语法就能写完。

struct CudaDeleter { void operator()(void* p) const noexcept { if (p) cudaFree(p); } };
using DeviceBuffer = std::unique_ptr<void, CudaDeleter>;

DeviceBuffer alloc_gpu(size_t bytes) {
    void* p = nullptr;
    if (cudaMalloc(&p, bytes) != cudaSuccess) throw std::bad_alloc{};
    return DeviceBuffer(p);
}

struct StreamGuard {
    cudaStream_t s;
    explicit StreamGuard() : s([]{ cudaStream_t t; cudaStreamCreate(&t); return t; }()) {}
    ~StreamGuard() { cudaStreamDestroy(s); }
    StreamGuard(const StreamGuard&) = delete;
    StreamGuard(StreamGuard&& o) noexcept : s(o.s) { o.s = nullptr; }
};

void infer(DeviceBuffer& in, DeviceBuffer& out) {
    StreamGuard stream;
    cudaMemcpyAsync(out.get(), in.get(), N, cudaMemcpyDeviceToDevice, stream.s);
    engine_->enqueueV2(buffers_, stream.s, nullptr);    // 异常也保证 stream 销毁
    cudaStreamSynchronize(stream.s);
}
资源 RAII 类型 deleter 多所有者支持
GPU 显存 unique_ptr<void, CudaDeleter> cudaFree 转 shared_ptr
共享 GPU buffer shared_ptr<void, CudaDeleter> 最后一个释放者 free make_shared + 引用计数
Stream 自定义类 StreamGuard cudaStreamDestroy 仅可 move
Event 自定义类 EventGuard cudaEventDestroy 仅可 move
TensorRT engine unique_ptr<ICudaEngine, TrtEngineDeleter> engine->destroy() 可 share 给多个 ctx

调研依据:NVIDIA CUDA C++ Programming Guide §”Streams” 强调”all CUDA resources must be released”;PyTorch 的 c10::cuda::CUDACachingAllocator、Megatron-LM 的 MemoryBuffer 类与 vLLM 的 Worker 内部全部采用 RAII + 智能指针模式管理显存,异常路径的零泄漏是 C++ 推理引擎相比 Python 框架的隐藏优势之一。

说明 · 本站内容均为学习笔记与经验总结,所有菜谱与技法请结合实际食材、季节与个人口味灵活调整。涉及生食、营养与健康的内容仅供参考,特殊体质或疾病请咨询专业营养师/医生。