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 框架的隐藏优势之一。