std::unique_ptr支持自定义deleter但必须显式声明模板类型,删除器类型编译期固定,lambda需用decltype捕获,有状态删除器需保证移动安全,且deleter必须noexcept。

std::unique_ptr 支持自定义析构函数(更准确地说,是自定义 Deleter),但必须在模板参数中显式声明类型,且调用方式和默认行为有关键差异——不写对模板参数或传错 callable 类型,编译器会直接报错,不是运行时报错。
std::unique_ptr 模板参数必须显式写出
很多人误以为可以只传一个 lambda 给构造函数就能“动态绑定”删除器,实际不行。std::unique_ptr 的删除器类型是模板参数的一部分,编译期就固定了。这意味着:
- 如果你用
std::default_delete<t></t>(即不写第二个模板参数),那 delete 行为就只能是delete ptr,无法更改 - 若要自定义,必须写成
std::unique_ptr<int mydeleter></int>或std::unique_ptr<int void></int>这类完整形式 - lambda 不能直接作为模板参数(除非用
decltype捕获其类型),更常见的是用函数指针、函数对象或std::function(但后者有额外开销)
错误示范:std::unique_ptr<int> p(new int(42), [](int* p){ std::cout —— 这段代码能编译通过,但 <code>Deleter 类型仍是 std::default_delete<int></int>,lambda 被忽略(仅用于构造内部的 deleter 对象,但模板参数没变,行为不变)。
函数对象作 Deleter:轻量且推荐
最常用、零开销的方式是定义一个可调用的 struct 或 class,重载 operator(),接收 T* 参数:
struct FileCloser {
void operator()(FILE* f) const noexcept {
if (f) fclose(f);
}
};
std::unique_ptr<file filecloser> fp(fopen("data.txt", "r"));
// 离开作用域时自动调用 FileCloser{}(fp.get())
</file>
注意点:
- 必须接受
T*(这里是FILE*),不能是const FILE*或引用 - 建议加
noexcept,因为std::unique_ptr析构函数要求deleter(ptr)不抛异常 - 该 struct 无状态时,sizeof 仍为 1(空基优化后通常不占空间)
函数指针作 Deleter:兼容 C 风格资源
适合封装已有的 C 函数,比如 free、close、dlclose:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
void my_free(void* p) { free(p); }
std::unique_ptr<int void> p(static_cast<int>(malloc(sizeof(int))), my_free);
// 注意:第二个模板参数必须匹配函数签名,不能写成 void(*)(int*)
</int></int>
常见坑:
- 函数签名必须严格匹配:参数是
void*,就不能写void(*)(int*),否则类型不兼容 - 如果资源类型是
int*,但释放函数是void(*)(void*),需确保传入时能隐式转为void*(通常可以) - 避免用
std::function包装函数指针——它引入虚调用和堆分配,违背unique_ptr的零成本原则
移动语义与自定义 Deleter 的交互
自定义 Deleter 不影响移动行为,但要注意:如果 Deleter 本身有状态(比如捕获了某个句柄或上下文),那移动构造/赋值时是否转移该状态,取决于你如何实现它的移动操作。
例如:
struct ContextDeleter {
int ctx_id;
ContextDeleter(int id) : ctx_id(id) {}
ContextDeleter(ContextDeleter&& other) noexcept : ctx_id(other.ctx_id) {
other.ctx_id = -1; // 清理源状态
}
void operator()(int* p) const noexcept {
if (p && ctx_id > 0) { /* use ctx_id */ delete p; }
}
};
std::unique_ptr<int contextdeleter> a(new int(1), ContextDeleter(123));
auto b = std::move(a); // b 拿到 ctx_id=123,a.ctx_id 变为 -1
</int>
重点在于:标准库不会帮你管理 Deleter 的状态,你得自己保证移动安全。无状态的函数对象或函数指针则完全不用操心。
最容易被忽略的一点:即使你用了自定义 Deleter,std::unique_ptr 的析构函数仍是 noexcept,所以你的 Deleter 必须也满足这个约束;否则一旦抛异常,程序会直接调用 std::terminate。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










