
1. 默认 copy 悄悄 double-free
LiDAR 驱动返回 DMA 映射指针,类里只写了析构 munmap,却忘了删 copy——vector<LidarBuffer> 扩容或按值传参时两份对象指向同一块物理页,析构二次释放,ASan 报 heap corruption,且偶发才复现。Rule of Five:自定义析构时,通常也要声明 copy/move 与 assignment;能 Rule of Zero 则优先 Zero。资源管理类先写 Five(或 Zero),再写业务——顺序反了,返工成本是半夜 field call。编译器生成的默认 copy 对 raw 指针是浅拷贝,对 unique_ptr 是 move 语义——混用成员时更要显式声明五法则,别假设「编译器会帮我做对」。相机 SDK 句柄、GPU context、serial fd 同属「非平凡资源」——五法则清单应在驱动封装 PR 模板里,而不是等 ASan 报 double-free 再补。
2. move-only DMA 包装
DmaBuffer:析构 free,copy delete,move 用 exchange 置源 ptr 为 nullptr。move 后源对象析构安全 no-op。socket fd 类同理:copy delete,move 转移 fd 并置源为 invalid。对齐分配用 aligned_alloc 时,释放必须配平台约定 API,别混 std::free 除非文档允许。move ctor 标 noexcept,让 vector<DmaBuffer> 扩容走 move 而非 silent copy 整块 DMA。
class DmaBuffer {
void* ptr_{nullptr};
size_t bytes_{0};
public:
DmaBuffer(void* p, size_t n) : ptr_(p), bytes_(n) {}
~DmaBuffer() { if (ptr_) platform_free(ptr_, bytes_); }
DmaBuffer(const DmaBuffer&) = delete;
DmaBuffer& operator=(const DmaBuffer&) = delete;
DmaBuffer(DmaBuffer&& o) noexcept
: ptr_(std::exchange(o.ptr_, nullptr)), bytes_(o.bytes_) {}
DmaBuffer& operator=(DmaBuffer&& o) noexcept {
if (this != &o) { reset(); ptr_ = std::exchange(o.ptr_, nullptr); bytes_ = o.bytes_; }
return *this;
}
};3. 不可移动的成员
含 mutex 或 thread 的类型通常 delete 全部 copy/move,或用 pimpl 把线程放进 unique_ptr<Impl>。jthread 可 move,但 shutdown 仍要 request_stop + join,别 detach。WorkerPool 误 move 等于把锁和线程句柄拆成两半——这类对象整个 delete move 比半移动安全。若必须可移动,用 pimpl:公开类只持 unique_ptr<Impl>,Impl 里放 mutex/thread,move 只转移 unique_ptr,语义清晰。
4. swap、noexcept 与 vector 扩容
资源类 swap 应 noexcept,让 vector<DmaBuffer> 扩容走 move 而非 copy。move assign 先 release 自有资源再 steal;自赋用 if (this==&other) 或 copy-and-swap。= default move 仅当所有成员可 move——含不可移动成员时会隐式 delete,有人改回 raw 指针绕过是事故源头。copy-and-swap idiom 对资源类尤其稳:copy ctor 深拷贝,swap 交换,assign 用 copy-and-swap 自动处理自赋与异常安全。
5. unique_ptr deleter 与驱动 API
C 驱动 driver_free 用 unique_ptr<void, decltype(&driver_free)> 比手写五法则更少 bug,deleter 随 move 转移。GPU/CUDA 同理;stream sync 在析构还是显式 sync() 由文档约定,释放路径必须在析构可达。MSVC 与 Linux 下 handle 类型不同,Rule of Five 结构相同——review 别只测一个平台。deleter 应 noexcept,否则 unique_ptr move 可能触发 unexpected copy 路径(旧标准库行为),机器人 hot path 里资源类 move 一律 noexcept。
6. Rule of Zero 何时够
能全用 vector、unique_ptr、标准容器成员则零自定义 special member——优先。只有 raw handle、GPU mem、驱动 DMA、内置 mutex/thread 才值得包装。legacy 资源 wrap 成 unique_ptr 自定义 deleter,Often 比完整五法则更少行、更少 bug。评审新问题:「能否用标准容器 + deleter 替代手写五法则?」能则 Zero,不能再写 Five 并 delete copy。Rule of Zero 的类进 std::vector、std::optional、返回值时行为与直觉一致——编译器生成的 move 逐成员 move,无需人工维护五函数一致性。
7. 失败症状与案例
默认 copy 两份 ptr → double free。move 后原对象仍 use → moved-from 非空指针被误读。现场曾出现「拷贝 DmaBuffer 进 std::map 键」——编译过、跑几百帧才崩;delete copy 后编译期就拦住。五法则类进 vector 但未 noexcept move,扩容 silent copy 大 DMA 块。节点 shutdown 时 resource 类析构顺序若与 driver 停采顺序打架,可能 double-free 或 use-after-free——文档写清「先 stop stream 再 release buffer」。把资源类放进 std::unordered_map 当 value 时,若 copy 未 delete,insert 与 rehash 都会触发浅拷贝——这是 map 场景下 double-free 的高频触发点,review 应 grep 资源类是否 delete copy。
8. 验收
ASan/LSan 长跑:频繁 move/析构无 leak。单测:copy deleted 应编译失败;move 后原对象 size==0 或 ptr==nullptr。grep:= default move 是否 noexcept;legacy raw handle 是否已包 RAII。新资源类 PR 必须说明选 Zero 还是 Five 的理由,别默认「能编译就行」。构造中途 throw:已打开资源仍被 deleter 释放(强异常安全或 noexcept 构造)。可在 CI 加 compile-only test:static_assert(!std::is_copy_constructible_v<DmaBuffer>),防止后人误恢复 default copy。
9. 与 shutdown、录包对齐
节点 shutdown 时 resource 类析构顺序若与 driver 停采顺序打架,可能 double-free 或 use-after-free。文档写清「先 stop stream 再 release buffer」。三/五法则不是背诵——是「谁拥有这块 mmap」在类型上的显式声明,漏一项就交给编译器默认 copy 埋雷。录包复盘若 crash 在 munmap,先查是否有未 delete 的 copy 或 moved-from 被误用。团队可维护「资源类 checklist」:析构释放、copy deleted 或深拷贝、move noexcept、swap noexcept、能否 Rule of Zero——新类对照勾选再 merge,比事后 code archeology 省 field 时间。
相关
也可以看看
johan's blog