目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day06 只做一件事:实现 make_shared<T>(args...) —— 对象与控制块一次分配。你已经在 Day04/Day05 建好了“原子双计数器 + AddRefLock + weak_ptr 生命周期”,今天就是把 Boost 经典的 inplace control block 落地,并用测试跑通构造/析构/异常路径。
1. 今日目标
在 Day05 的 weak_ptr 体系之上,新增 make_shared 这条“单次分配路径”:
- 实现
my::make_shared<T>(args...):对象与控制块 一次分配(更少 malloc,更缓存友好) - 控制块新增“内嵌对象”版本:
aligned_storage + placement new - 测试闭环:功能 / 析构次数 / 异常安全 + 演示 weak_ptr 下的“延迟释放”副作用
核心收获:把“对象生命周期(use_count)”与“控制块生命周期(weak_count)”的分离,推进到 make_shared 的现实权衡:更快,但可能更“占内存更久”。
2. 今日设计:make_shared 的内存模型与权衡
2.1 为什么 make_shared 更快?
传统构造(两次分配):
new T(args...):分配对象内存 + 构造对象SharedPtr构造:再分配控制块内存(use/weak 计数)
两块内存往往不连续,额外一次分配也更贵。
make_shared(一次分配):
- 只
new一次控制块,控制块内部留出一块对齐良好的原始存储(storage) - 用 placement new 在 storage 里构造
T - 对象析构时只析构,不释放那块内存;等 weak_count 归零再释放整块控制块
2.2 内存布局(单块内存)
┌─────────────────────────────────────┐
│ SpCountedBase │ use_count_, weak_count_ (原子)
├─────────────────────────────────────┤
│ storage_(aligned_storage 原始内存) │ 用来“就地”放 T
└─────────────────────────────────────┘
2.3 Dispose vs Destroy:为什么必须分离?
use_count_ == 0:对象该死了 →Dispose()(调用~T())weak_count_ == 0:控制块没人引用了 →Destroy()(delete this释放整块内存)
这正是 make_shared 的 trade-off 来源:
如果还有 weak_ptr 活着,即便对象已经析构,控制块那整块内存也要等到 weak_count==0 才能释放。
3. 关键实现(只贴关键代码,全部来自你今天的“真实仓库”)
关键文件:
include/sp_counted_impl.h(新增SpCountedImplPdi<T>)include/shared_count.h(新增SharedCount(sp_inplace_tag<T>, ...)+GetInplacePointer())include/my_shared_ptr.h(新增 inplace 构造路径)include/my_make_shared.h(新增make_shared工厂)
3.1 新增:inplace 控制块 SpCountedImplPdi<T>
它做三件事:准备 storage → placement new 构造对象 → Dispose 显式析构对象。
// include/sp_counted_impl.h
template <typename T>
class SpCountedImplPdi : public SpCountedBase {
private:
typename std::aligned_storage<sizeof(T), alignof(T)>::type storage_;
public:
template <typename... Args>
explicit SpCountedImplPdi(Args&&... args) {
::new (static_cast<void*>(&storage_)) T(std::forward<Args>(args)...);
}
T* GetPoint() noexcept { return reinterpret_cast<T*>(&storage_); }
T* get_pointer() noexcept { return GetPoint(); } // 兼容调用点命名
void Dispose() noexcept override {
GetPoint()->~T(); // 只析构对象,不释放内存
}
};
要点:
aligned_storage:只保证“大小够 + 对齐够”,不构造对象- placement new:只构造,不分配
Dispose():只析构对象;控制块内存交给Destroy()(基类默认delete this)
3.2 新增:SharedCount 的 inplace 构造(tag dispatch)
今天的实现用 sp_inplace_tag<T> 强制走 make_shared 的构造路径,避免和其他构造(裸指针/删除器)模板重载打架。
// include/shared_count.h
template <typename T>
struct sp_inplace_tag {};
template <typename T, typename... Args>
explicit SharedCount(sp_inplace_tag<T> tag, Args&&... args)
: control_block_(nullptr) {
(void)tag;
typedef SpCountedImplPdi<T> ImplType;
control_block_ = new ImplType(std::forward<Args>(args)...); // 一次分配
}
3.3 从控制块里“拿到对象指针”:GetInplacePointer
这是 make_shared 路径里 SharedPtr::ptr_ 的来源:指向控制块内嵌 storage 里的 T。
// include/shared_count.h
template <typename T>
T* GetInplacePointer() noexcept {
typedef SpCountedImplPdi<T> ImplType;
ImplType* point = static_cast<ImplType*>(control_block_);
return point ? point->get_pointer() : nullptr;
}
3.4 SharedPtr 新增:inplace 构造路径(从控制块取指针)
// include/my_shared_ptr.h
template <typename Y>
SharedPtr(detail::sp_inplace_tag<Y>, detail::SharedCount const& control_block) noexcept
: ptr_(nullptr), count_(control_block) {
if (!count_.empty()) {
ptr_ = const_cast<detail::SharedCount&>(control_block)
.template GetInplacePointer<Y>();
}
}
这一段今天真实踩过坑:如果
SpCountedImplPdi没有正确继承SpCountedBase/Dispose签名对不上 /get_pointer名字不一致,就会出现一连串 “override 不匹配 / 指针不能转换 / static_cast 非法”。
3.5 make_shared 工厂函数:把控制块与 SharedPtr 串起来
// include/my_make_shared.h
template <typename T, typename... Args>
SharedPtr<T> make_shared(Args&&... args) {
detail::SharedCount control_block(
detail::sp_inplace_tag<T>{},
std::forward<Args>(args)...);
return SharedPtr<T>(detail::sp_inplace_tag<T>{}, control_block);
}
4. 单元测试:功能 + 析构 + 异常安全 + 延迟释放演示
今天的 test/test_make_shared.cc 已经把 make_shared 路径跑得很完整,关键覆盖点:
- 基本构造/成员访问:
make_shared<TestObject>(...) - 多参数转发:
make_shared<Point>(10,20,30) - 拷贝/移动:
use_count变化正确 - weak_ptr 配合:
expired()/lock()正确 - 异常安全:构造函数抛异常能捕获、流程不崩溃
- 延迟释放演示:weak_ptr 存活期间,控制块整块内存不释放(对象已析构)
运行输出全绿(assert 未触发),说明 Day06 主链路已经闭环。
5. 开发过程中遇到的关键问题(替换为 Day06 真实问题)
问题 1:CMake 报 “找不到 test/benchmark.cc”
定位原因:CMakeLists.txt 里 add_executable(benchmark test/benchmark.cc),但仓库里没有这个文件。
最小修改点:要么补一个 test/benchmark.cc 占位,要么暂时注释 benchmark target(不影响功能测试)。
补必要测试/断言:无(构建系统问题)。
提醒坑:Day06 先保证 test_make_shared 可跑;benchmark 可以后置,不要阻塞主链路。
问题 2:SharedPtr<void>(mem, deleter) 编译失败:SharedCount 找不到匹配构造
定位原因:SharedCount 的“删除器路径”是 tag dispatch 版本:
SharedCount(sp_deleter_tag{}, ptr, deleter)
但 SharedPtr(Y*, D) 最初写成了 count_(ptr, deleter),没有带 tag。
最小修改点(库代码):让 SharedPtr(Y*, D) 显式走 tag:
explicit SharedPtr(Y* ptr, D deleter)
: ptr_(ptr), count_(detail::sp_deleter_tag{}, ptr, deleter) {}
补必要测试/断言:现有的 test_void_pointer() 就是回归测试(它能跑通说明修复生效)。
提醒坑:别随手给 SharedCount(P, D) 再加一个构造去“偷懒”,后面很容易和 inplace / 裸指针构造发生模板匹配歧义。
问题 3:Dispose() override but does not override + ImplType* 不能转成 SpCountedBase*
定位原因:inplace 控制块这一套在“类层次/命名”上没对齐:
SpCountedImplPdi<T>没有正确public SpCountedBase(导致指针不能上转)- 或者实现了
dispose()(小写)而基类要求Dispose()(大写),签名对不上(导致 override 报错) GetInplacePointer()调get_pointer(),但实现里叫别的名字(导致找不到成员)
最小修改点:把SpCountedImplPdi<T>统一到SpCountedBase体系,并补一个get_pointer()兼容别名(你今天已这么做)。
补必要测试/断言:现有 make_shared 的全套测试足够覆盖(Point/TestObject/MayThrow/LargeObject 都会实例化 PDI)。
提醒坑:这类错误一旦出现不要被“海量模板报错”吓住——先抓第一条override/ “不能上转” 的根因,后面的基本都是连锁。
问题 4:为什么演示里“对象已析构但内存未释放”?
定位原因:make_shared 的对象内存与控制块同一块内存。对象析构发生在 Dispose(),但释放整块内存要等 weak_count==0 的 Destroy()。
最小修改点:这是设计权衡,不是 bug。
补必要测试/断言:你已经用 LargeObject + WeakPtr 把现象演示清楚。
提醒坑:对象很大且 weak_ptr 可能长期存在时,要评估是否用传统 SharedPtr(new T) 更合适。
6. Day 06 学习检查清单(今日 6 问)
Q1:理解为什么 make_shared 更快?
因为把 “对象内存分配” 合并进 “控制块分配” —— 一次 new 搞定控制块+对象存储,并改善局部性(更少 malloc、更少碎片、更缓存友好)。
Q2:能解释 aligned_storage 的作用?
它提供一块 大小为 sizeof(T)、对齐为 alignof(T) 的原始内存:只负责“能放下 T”,不负责构造。适合控制块里做“内嵌对象”的容器。
Q3:知道 placement new 的用法?
::new (addr) T(args...):在指定地址 addr 上构造对象(不分配内存)。析构要手动 p->~T(),内存释放由外层负责。
Q4:理解 Dispose() 和 Destroy() 的区别?
Dispose():对象生命周期结束时调用(use_count 归零)—— 析构对象Destroy():控制块生命周期结束时调用(weak_count 归零)—— 释放控制块内存
make_shared 的对象存储在控制块里,所以更必须分离两者。
Q5:能说出 make_shared 的缺点?
weak_ptr 存活时会导致 整块内存延迟释放(对象虽析构,但控制块仍占着那块内存)。另外 make_shared 路径通常不支持“每次实例自定义删除器”。
Q6:知道何时不应该用 make_shared?
- 对象非常大、且 weak_ptr 可能长期存在(内存占用更久)
- 需要自定义删除器(文件句柄、特殊释放逻辑)
- 需要特殊构造/私有构造受限(此类场景常需要定制工厂)
7. 今日迭代进度
✅ Day06 已完成:make_shared 主链路闭环(功能测试全绿)
SpCountedImplPdi<T>:aligned_storage + placement new + Dispose 显式析构SharedCount(sp_inplace_tag<T>, ...):inplace 控制块构造路径落地SharedPtr(in_place_tag, SharedCount):从控制块取内嵌对象指针make_shared<T>(args...):工厂函数串起全链路- 测试通过:基本用法 / 多参数 / 拷贝移动 / weak_ptr 配合 / 异常安全 / 延迟释放演示