Day 06|make_shared:inplace 控制块 + aligned_storage,一次分配把对象“塞进”控制块

4 阅读7分钟

目标:用 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 更快?

传统构造(两次分配):

  1. new T(args...):分配对象内存 + 构造对象
  2. 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.txtadd_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==0Destroy()
最小修改点:这是设计权衡,不是 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 配合 / 异常安全 / 延迟释放演示