21. 自定义删除器:函数对象、lambda表达式
智能指针的删除器,说白了就是「资源释放的最后一道关卡」。
默认情况下,std::unique_ptr 和 std::shared_ptr 用 delete 来释放内存。但现实世界哪有这么简单?我做过一个嵌入式项目,内存是从特定池子里分配的,必须调用 pool_free() 来归还。这时候默认删除器就完全不够用了。
为什么需要自定义删除器?
你想想看,下面这些场景,默认的 delete 能搞定吗?
- 非堆内存:比如用
malloc分配的内存,得用free释放 - 文件句柄:
fopen打开的文件,得用fclose关闭 - 自定义分配器:从对象池、内存池借来的资源,得归还
- 特殊清理逻辑:释放前需要写日志、更新统计信息
嗯,这时候就需要自定义删除器登场了。
核心思想:删除器是一个可调用对象,智能指针在析构时会调用它来释放资源。你给它什么,它就执行什么。
三种实现方式
我个人习惯把自定义删除器分成三类:函数指针、函数对象、lambda 表达式。咱们一个一个看。
1. 函数指针
最直接的方式,写一个普通函数当删除器。
// 自定义删除函数
void custom_deleter(int* p) {
std::cout << "自定义删除器被调用,释放地址: " << p << std::endl;
delete p;
}
int main() {
std::unique_ptr<int, decltype(&custom_deleter)>
ptr(new int(42), custom_deleter);
// 离开作用域时,自动调用 custom_deleter
return 0;
}
注意这里 decltype(&custom_deleter) 指定了删除器类型。为什么是函数指针类型?因为 unique_ptr 的删除器是模板参数的一部分,必须在编译期确定。
注意:函数指针作为删除器有个小坑——它本身不携带状态。如果你需要在删除时访问某些上下文信息(比如日志对象、计数器),函数指针就无能为力了。
2. 函数对象(仿函数)
函数对象可以携带状态,这是它最大的优势。
// 带状态的删除器
struct FileDeleter {
bool log_enabled;
explicit FileDeleter(bool log) : log_enabled(log) {}
void operator()(FILE* file) const {
if (log_enabled) {
std::cout << "[日志] 关闭文件句柄" << std::endl;
}
if (file) {
fclose(file);
std::cout << "文件已关闭" << std::endl;
}
}
};
int main() {
// 打开文件
FILE* f = fopen("test.txt", "w");
if (!f) return -1;
// 使用带日志的删除器
std::unique_ptr<FILE, FileDeleter>
ptr(f, FileDeleter(true));
// 离开作用域时,自动关闭文件并记录日志
return 0;
}
我在项目中遇到过类似场景:需要统计每个智能指针释放了多少次,释放时还要把信息写入监控系统。函数对象完美解决了这个问题——把计数器、日志句柄都塞进删除器里。
3. Lambda 表达式
说实话,这是我现在最常用的方式。简洁、直观、零开销。
int main() {
// 用 lambda 作为删除器
auto deleter = [](int* p) {
std::cout << "lambda 删除器: " << *p << std::endl;
delete p;
};
std::unique_ptr<int, decltype(deleter)>
ptr(new int(100), deleter);
// 也可以直接写
auto ptr2 = std::unique_ptr<int, void(*)(int*)>(
new int(200),
[](int* p) {
std::cout << "内联 lambda" << std::endl;
delete p;
}
);
return 0;
}
为什么 lambda 这么好用?因为它本质上是匿名函数对象,编译器会为它生成一个独一无二的类型。而且 lambda 可以捕获外部变量,相当于自带状态。
小技巧:如果 lambda 不捕获任何变量,它可以隐式转换为函数指针。这时候你可以用 void(*)(T*) 作为删除器类型,避免模板参数膨胀。
shared_ptr 的特殊之处
这里有个重要区别:shared_ptr 的删除器不是模板参数,而是构造函数的参数。这意味着删除器类型不会影响 shared_ptr 的类型。
// shared_ptr 的删除器是运行时绑定的
auto sp1 = std::shared_ptr<int>(new int(1), [](int* p) {
std::cout << "删除器A" << std::endl;
delete p;
});
auto sp2 = std::shared_ptr<int>(new int(2), [](int* p) {
std::cout << "删除器B" << std::endl;
delete p;
});
// 类型相同!可以放进同一个容器
std::vector<std::shared_ptr<int>> vec;
vec.push_back(sp1);
vec.push_back(sp2);
嗯,这里要注意:shared_ptr 通过类型擦除(type erasure)来存储删除器,所以会有微小的运行时开销。但换来的是灵活性——你可以在运行时决定用什么删除器。
避坑指南
我曾经踩过一个坑,分享给你:
- 删除器必须可调用:别传个 nullptr 或者不完整的类型,编译期直接报错
- 异常安全:删除器不应该抛出异常。如果必须抛,记得在删除器内部 catch 住
- 空指针检查:删除器被调用时,智能指针内部已经判断过非空了,但如果你手动调用
reset()传了空指针,删除器还是会被调用 - 移动语义:
unique_ptr移动时,删除器也会跟着移动。如果删除器是 lambda 且捕获了引用,要小心悬空引用
重要:std::make_shared 不支持自定义删除器。如果你需要自定义删除器,必须直接用 std::shared_ptr 的构造函数。这是 make_shared 的一个已知限制。
知识体系图
下面这张图帮你理清自定义删除器的整体脉络:
总结
自定义删除器是智能指针的「瑞士军刀」。函数指针适合简单场景,函数对象适合需要状态的场景,lambda 则是日常开发的首选。
我个人建议:除非你有明确的理由用函数对象,否则一律用 lambda。代码更短、意图更清晰、维护成本更低。
记住一句话:删除器是你的资源释放策略,别让它成为你的 bug 来源。
一句话总结:自定义删除器让智能指针不再局限于 new/delete,而是可以管理任何需要「获取-释放」模式的资源。
公众号:蓝海资料掘金营,微信deep3321