目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day03 只做一件事:在 Day02 控制块分离架构上,加入“自定义删除器(deleter)”。
1. 今日目标
今天要把“释放策略”从 Day02 的默认 delete 扩展为可自定义:
- 新增带删除器的控制块:
SpCountedImplPointerd<P, D> - 扩展
SharedCount:支持(p, deleter)构造,创建对应控制块 - 扩展
SharedPtr:支持SharedPtr(p, deleter)、Reset(p, deleter) - 理解“类型擦除”的第二层:删除器类型也被擦除
- 支持管理非
new/delete资源:数组delete[]、文件fclose、void*自定义释放等
核心收获:SharedPtr 本体继续保持“薄”,释放策略跟随控制块生命周期。
2. 今日设计:控制块扩展(从 Day02 到 Day03)
2.1 Day02 的局限
Day02 的默认控制块是 SpCountedImplPointer<Y>,Dispose() 里固定做 delete。这意味着:
- 数组
new T[n]需要delete[],默认delete不匹配 - 文件句柄
FILE*需要fclose,默认delete完全不对 void*的真实类型丢了,需要自定义释放逻辑
本质:释放策略不应写死在 shared_ptr 里,更不应写死为 delete。
2.2 Day03 的方案:把删除器存进控制块
Day03 新增一条控制块实现:
SpCountedImplPointer<Y>:默认 delete(Day02)- ✅
SpCountedImplPointerd<P, D>:存P ptr_ + D deleter_,Dispose()执行deleter_(ptr_)
这样 shared_ptr 不需要知道“怎么删”,只要把释放动作交给控制块的虚函数即可。
3. 关键实现
关键文件:
include/sp_counted_base.hpp
include/sp_counted_impl.hpp
include/shared_count.hpp
include/my_shared_ptr.hpp
test/test_custom_deleter.cpp
3.1 自定义删除器控制块:SpCountedImplPointerd<P, D>
include/sp_counted_impl.hpp:
template <typename P, typename D>
class SpCountedImplPointerd : public SpCountedBase {
private:
P ptr_;
D deleter_;
public:
SpCountedImplPointerd(P p, D d) : ptr_(p), deleter_(d) {}
void Dispose() noexcept override { deleter_(ptr_); }
};
要点:
P是“资源句柄类型”,可以是int* / FILE* / void*D是“删除器类型”,可以是函数指针/仿函数/lambda/带状态对象- 删除器按值存储,确保它能活到最后一次释放
3.2 SharedCount:新增 (p, deleter) 构造
include/shared_count.hpp:
template <typename P, typename D>
explicit SharedCount(P p, D d) : control_block_(nullptr) {
if (p) {
control_block_ = new SpCountedImplPointerd<P, D>(p, d);
}
}
这一步把“创建哪种控制块”的决策统一收口到 SharedCount:
- 默认
SharedCount(p)→SpCountedImplPointer<T> - 新增
SharedCount(p, d)→SpCountedImplPointerd<P, D>
3.3 SharedPtr:支持 SharedPtr(p, deleter) / Reset(p, deleter)
include/my_shared_ptr.hpp:
template <typename Y, typename D>
explicit SharedPtr(Y* p, D d) : ptr_(p), count_(p, d) {}
template <typename Y, typename D>
void Reset(Y* p, D d) noexcept {
SharedPtr(p, d).Swap(*this);
}
依然沿用 Day02 的“临时对象 + Swap”风格,状态切换干净、不容易写出泄漏/重入 bug。
3.4 SharedPtr<void>:禁用 operator*
为了支持 SharedPtr<void> 做“只管理生命周期”,同时禁止 *p 这种无意义操作,你用 SFINAE 把 operator* 在 T=void 时移除:
template <typename U = T>
typename std::enable_if<!std::is_void<U>::value, U&>::type operator*() const noexcept {
return *ptr_;
}
效果:
SharedPtr<int>有operator*()SharedPtr<void>没有operator*()(写*p会编译报错)
4. 单元测试:验证 Day03 闭环正确
test/test_custom_deleter.cpp 覆盖了 8 组典型资源:
- 函数指针删除器:
SharedPtr<int>(new int(42), CustomDelete) - 仿函数删除器:数组
delete[] - lambda 删除器:数组
delete[] - 文件句柄:
FILE* + std::fclose - 带状态删除器:
LoggingDeleter("Memory-1/2") Reset(p, deleter):reset 先释放旧资源再接管新资源void* + 删除器:deleter 内部恢复真实类型并释放- no-op 删除器:栈对象不 delete,退出作用域仍有效
5. 开发过程中遇到的关键问题
问题 1:为什么会报 “no matching function for SharedCount(p, d)”?
因为 Day02 的 SharedCount 只有 SharedCount(T*)。当你新增 SharedPtr(p, d) 时,构造链会走到 count_(p, d),如果 SharedCount(P, D) 没实现,编译器就找不到匹配构造函数。
补上 SharedCount(P, D) 后,才能在这里创建 SpCountedImplPointerd<P, D> 控制块。
问题 2:为什么删除器必须放进控制块,而不是放在 SharedPtr 里?
释放动作发生在“最后一个 shared_ptr 释放时”。此时最后活着的是哪一个 SharedPtr 实例不可预测,如果删除器存放在某个 shared_ptr 对象里,它可能早已析构,导致释放时“找不到删除器/删除器悬空”。
控制块的生命周期与“所有权生命周期”同步,把删除器放在控制块里能保证:
- 删除器一定活到最后一次释放
- 所有副本共享同一释放策略
- 复制 shared_ptr 仍然很轻(只增计数,不复制删除器)
问题 3:SharedPtr<void> 为什么要禁用 operator*?
void 没有对象语义,解引用没有意义也不安全。你用 enable_if<!is_void> 让 operator* 在 T=void 时直接“消失”,从接口层面阻止误用:
SharedPtr<void> 只负责生命周期管理,访问必须通过用户自己显式转换/自定义逻辑完成。
问题 4:类型擦除是不是都这样实现的?
不一定都用虚函数,但核心结构几乎一致:外层只保存统一入口(基类指针/函数表),真实类型封装在模板生成的实现体里。
你这里属于经典 “基类 + virtual Dispose()” 的做法:
- Day02 擦除“真实对象类型 Y”(释放按 Y)
- Day03 再擦除“删除器类型 D”(释放按 D)
问题 5:删除器能不能“移动”进控制块?
可以。你当前实现是按值传参,效果更像“拷贝进控制块”。如果要支持更广泛删除器(比如 move-only 删除器),就需要把接口升级成 D&& + decay + std::forward:右值删除器移动进控制块,左值删除器拷贝进控制块。
6. Day 03 检查清单(复盘 Q&A + 延伸)
Q1:理解 SpCountedImplPointerd 的两个模板参数 P 和 D?
A:P 是资源句柄类型(不一定是 T*,可以是 FILE* / void*),D 是删除器对象类型(函数指针/仿函数/lambda)。控制块把“资源 + 释放策略”绑定在一起,Dispose() 统一做 deleter_(ptr_)。
Q2:能解释删除器是如何被“擦除”类型的?
A:SharedPtr/SharedCount 外层只持有 SpCountedBase*。真实删除器类型 D 被封装进 SpCountedImplPointerd<P, D>,析构时外层只调用 Dispose(),靠虚函数分发到正确的 deleter_(ptr_)。
Q3:知道如何管理文件句柄、数组等非 new 资源?
A:关键是“释放方式要匹配创建方式”。数组用 delete[] 的 deleter,文件句柄用 fclose 的 deleter,void* 用 deleter 恢复真实类型后释放;栈对象或外部资源用 no-op deleter 防止误删。
Q4:理解为什么删除器要拷贝到控制块中?
A:因为释放发生在最后一个 shared_ptr 释放时,删除器必须活到那一刻;控制块与所有权同寿命且被所有副本共享,所以 deleter 放控制块最稳。拷贝/移动进控制块,本质都是“按值持有”。
Q5:能画出带删除器的 shared_ptr 内存布局图?
SharedPtr<T>:
T* ptr_ // 访问按 T
SharedCount count_ // 持有 SpCountedBase* 控制块入口
控制块(派生实现体):
(SpCountedBase) use/weak
P ptr_ // 真实资源句柄
D deleter_ // 删除器对象
延伸 Q6:删除器能不能移动进控制块?
A:能;升级为 D&& + decay + forward 即可支持右值移动、甚至 move-only 删除器。
延伸 Q7:为什么不在 SharedPtr 每次拷贝时也把删除器拷贝一份?
A:会让 shared_ptr 变大、拷贝变重,还会强迫删除器可拷贝;控制块只存一份删除器,shared_ptr 拷贝只做计数增减,更符合 shared_ptr 的设计目标。
延伸 Q8:SharedPtr<void> 为什么不提供 operator*?
A:解引用 void 没语义且危险,禁用能把错误挡在编译期;如果确实要访问,需要用户显式转型或在 deleter/业务逻辑里保证类型正确。
7. 今日迭代进度
✅ Day03 已完成:自定义删除器支持
SpCountedImplPointerd<P, D>控制块落地(控制块保存删除器对象)SharedCount(P, D)构造补齐(打通构造链)SharedPtr(p, deleter)、Reset(p, deleter)跑通- 覆盖 8 类资源场景测试:数组 / 文件 / void* / no-op / 带状态删除器等
SharedPtr<void>通过 SFINAE 禁用operator*,避免误用
、