Day 07|别名构造 + pointer_cast + 比较运算符:把 shared_ptr 接口补齐

7 阅读9分钟

目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。

Day07 我主要干一件事:把 shared_ptr/weak_ptr 的“对外接口”补齐到能像 Boost/标准库一样顺滑使用。今天我重点对齐了 Boost 的几个关键设计:别名构造(Aliasing Constructor) 、三种 *_pointer_castnullptr 支持、比较运算符与 swap/reset,再做一轮工程化收尾(命名一致、坏味道处理)。核心理解也很统一:共享同一个控制块,不创建新对象。


1. 今日目标

在 Day06 已经跑通 make_shared(inplace 控制块 + placement new)之后,Day07 我把精力放在“接口闭环”上:

  1. 补齐 shared_ptr/weak_ptr 的收尾接口Reset/Swap、判空(operator bool / == nullptr / != nullptr)、跨类型相等比较等。
  2. 实现类型转换工具static_pointer_cast / dynamic_pointer_cast / const_pointer_cast,并保证转换后共享控制块
  3. 工程化收尾:整理头文件依赖、把命名统一(避免 shared_ptr/SharedPtr 混用)、把一些容易误写的点(比如 nullptr_toperator!= 真值表)彻底对齐。

核心收获:我现在能清楚地解释——

pointer_cast 并不是“创建新对象”,而是“创建一个转换后的 shared_ptr 句柄”,并且它和原 shared_ptr 共享同一个控制块


2. 今日设计:别名构造与 pointer_cast 的统一模型

2.1 别名构造(Aliasing Constructor)到底解决什么?

我今天最关键的认知是:shared_ptr 里保存的指针(ptr)和“拥有者”(控制块)可以不是同一个东西

别名构造的语义我用一句话记住:

我让 shared_ptr 的 get() 指向一个“子对象/成员指针 p”,但它的生命周期跟随另一个 owner shared_ptr 的控制块。

举个我自己最常用的场景:
我想拿 Personname 成员指针,但我必须保证 Person 没死:

SharedPtr<Person> person = make_shared<Person>("Alice", 30);
SharedPtr<std::string> name_ptr(person, &person->name);  // 别名构造
  • name_ptr.get()&person->name
  • name_ptr 析构时不会 delete 这个成员指针
  • 它只是在释放控制块引用,最终由控制块决定何时销毁整个 Person

2.2 三种 pointer_cast 的本质:cast 指针 + 别名构造

我把它们统一成一个模式来理解:

  1. r.get() 拿到裸指针
  2. 做一次 cast 得到 p
  3. 别名构造 SharedPtr<T>(r, p) 返回

所以无论 static/dynamic/const,区别只在“第 2 步怎么 cast”,第 3 步完全一致。


3. 关键实现

我今天的实现重点就是:别名构造 + pointer_cast + nullptr/比较/swap 的接口补齐。下面只贴最关键的“形态”,帮助我以后复习。

3.1 别名构造(Aliasing Constructor)

template <typename Y>
SharedPtr(const SharedPtr<Y>& owner, element_type* p) noexcept
    : ptr_(p), count_(owner.count_) {}

我记住它的要点:

  • ptr_ 存的是 p(子对象/转换后的指针)
  • count_ 拿的是 owner 的控制块(引用计数共享)

3.2 static / dynamic / const pointer_cast

static_pointer_cast(编译期转换,不检查):

template <typename T, typename U>
SharedPtr<T> static_pointer_cast(const SharedPtr<U>& r) noexcept {
  T* p = static_cast<T*>(r.get());
  return SharedPtr<T>(r, p);  // 别名构造,共享控制块
}

dynamic_pointer_cast(运行期检查,可能失败):

template <typename T, typename U>
SharedPtr<T> dynamic_pointer_cast(const SharedPtr<U>& r) noexcept {
  T* p = dynamic_cast<T*>(r.get());
  if (p) return SharedPtr<T>(r, p);
  return SharedPtr<T>();  // 失败:返回空
}

const_pointer_cast(移除 const,只改变视角):

template <typename T, typename U>
SharedPtr<T> const_pointer_cast(const SharedPtr<U>& r) noexcept {
  T* p = const_cast<T*>(r.get());
  return SharedPtr<T>(r, p);
}

我对它们的总结是:

这三个都会“多创建一个 SharedPtr 句柄”(引用计数 +1),但不会创建新对象,也不会创建新控制块。


4. 单元测试:功能闭环(别名构造 / cast / nullptr / 容器 / swap)

我今天跑的 test_complete 覆盖了这些核心点:

  • 别名构造name_ptr/age_ptr 共享 person 控制块,use_count 预期一致;person.reset() 后成员指针仍可用(生命周期正确)。
  • static_pointer_cast:向上/向下转换都共享控制块;通过基类指针调用虚函数,实际调用派生类实现。
  • dynamic_pointer_cast:成功时共享控制块;失败时返回空指针,同时原 base 不受影响。
  • nullptr 支持SharedPtr<T> p = nullptr;、比较 p == nullptr / nullptr != p 等都能用。
  • 比较运算符 + 关联容器std::set/std::map 能用 SharedPtr 当 key,排序/去重行为符合“按地址身份”的语义。
  • swap:成员 swap + std::swap 都能正常交换指针与控制块。

5. 开发过程中遇到的关键问题

我按:定位原因 → 最小修改点 → 补必要测试/断言 → 提醒坑 来记录

问题 1:std::nullptr_t 写错(写成 std::nullptr t)导致 IDE 标红/编译错误

定位原因:
nullptr 是关键字(值),它的类型是 std::nullptr_t。我如果少写了 _t,就等于写了一个不存在的类型 std::nullptr,自然报错。另外如果忘了 #include <cstddef>,也可能找不到 std::nullptr_t

最小修改点:

  • 统一改成 std::nullptr_t
  • 头文件补 #include <cstddef>

补必要测试/断言:
我必须覆盖“拷贝初始化”的那条路,因为这最容易踩坑:

SharedPtr<int> p = nullptr;  
EXPECT_TRUE(p == nullptr);
EXPECT_TRUE(nullptr == p);
EXPECT_FALSE(p != nullptr);
EXPECT_FALSE(nullptr != p);

提醒坑:
SharedPtr<int> p(nullptr);(直接初始化)和 SharedPtr<int> p = nullptr;(拷贝初始化)走的规则不一样。为了对齐标准库体验,我需要专门支持 nullptr_t 构造。


问题 2:operator!= (nullptr, p) 真值表写反(复制粘贴坑)

定位原因:
我一开始写 p == nullptrreturn !p;,然后写 != 时复制粘贴也写成 return !p;,结果 != 的语义就反了。

正确真值表我强制记住:

  • p == nullptr!p
  • p != nullptrbool(p)

最小修改点:
!= 必须返回 static_cast<bool>(p)(或 p.get() != nullptr)。

补必要测试/断言:

SharedPtr<int> e;
EXPECT_FALSE(nullptr != e);

auto p = make_shared<int>(1);
EXPECT_TRUE(nullptr != p);

提醒坑:
这类 bug 不会“看起来崩掉”,但会让很多 if 判断逻辑悄悄错掉,所以必须用断言把它钉死。


问题 3:operator<std::common_type<T*, U*> 可能导致模板实例化失败

定位原因:
T*U* 没有公共类型(不相关指针类型)时,std::common_type 可能没有 type,模板会直接炸,报错还很“模板风暴”。

最小修改点:
为了让 SharedPtr<T>/SharedPtr<U>< 更稳(尤其用于 set/map),我更倾向于用:

std::less<const void*>()(a.get(), b.get());

这个写法跨类型更稳,也更贴近“按地址身份排序”的语义。

补必要测试/断言:
我用 set/map 的测试验证:

  • set.insert 去重是否合理(同地址不会重复)
  • map.size() 是否符合预期

提醒坑:
这里的排序比较的是“指针身份”,不是对象值;如果以后我想按对象内容排序,那就必须自定义 comparator(解引用比较),不能改动 operator< 的语义。


问题 4:const_pointer_cast 之后修改对象到底是否合法的吗?

定位原因:
const_pointer_cast 只是“把指针视角从 const 改成非 const”,它不改变对象本体是否 const
如果对象本体是 const T(例如 make_shared<const T> 创建的),我 cast 后再写它,严格来说是 UB

最小修改点(推荐测试写法):
我更推荐用“对象本体非 const + 只读视图”的方式测试:

  • make_shared<T> 创建非 const 对象
  • 再构造 SharedPtr<const T> 只读视图
  • const_pointer_cast<T> 写入,验证变化

补必要测试/断言:

  • get() 地址必须相同(同一对象)
  • 修改后从只读视图读出来的值也变化(证明共享对象)

提醒坑:
“能跑”不代表“标准意义正确”,我后续如果要把这套库当作品展示,最好不要在示例里示范 UB。


6. Day 07 学习检查清单(今日 5 问|我自己的回答)

Q1:我理解别名构造的用途了吗?

答:
我现在能明确说:别名构造是用来创建“视图型 shared_ptr”的。它能让我让 SharedPtr<T> 指向成员/子对象/转换后的指针 p,但生命周期仍然绑定在 owner 的控制块上。
它解决了“成员指针悬垂”和“成员指针被错误 delete”的问题,也让 pointer_cast 变得非常自然。


Q2:我能解释类型转换如何共享控制块吗?

答:
我把它记成固定模板:

  • p = cast(r.get())
  • return SharedPtr<T>(r, p)(别名构造)

因此转换会:

  • 新建一个转换后的 SharedPtr 句柄(引用计数 +1)
  • 共享同一个控制块(不创建新控制块)
  • 不创建新对象(对象仍然是同一个)

Q3:我知道 dynamic_cast 失败时返回什么吗?

答:
指针版 dynamic_cast<T*>(...) 失败返回 nullptr。所以 dynamic_pointer_cast 失败时返回空 SharedPtr<T>(),而原来的 SharedPtr<U> 不受影响(引用计数不变,对象依然被原指针管理)。


Q4:我理解为什么比较用 std::less 吗?

答:
因为我想让 SharedPtr 能作为 std::set/std::map 的 key,就必须提供稳定的严格弱序。
operator< 的语义在这里就是“按指针身份排序”,而 std::less 是标准库提供的稳定比较器工具,用它比较地址更稳、更适配泛型/关联容器的场景(尤其跨类型时避免 common_type 的边界问题)。


Q5:我知道如何在 std::map 中使用 shared_ptr 吗?

答:
我知道:map 的 key 比较的是 指针身份(地址) ,不是对象值。
所以:

  • 两个 make_shared<int>(42) 就算值相同,也是两个不同 key(不同地址)
  • 如果两个 SharedPtr 共享同一对象(同地址),那就是同一个 key
    如果以后我想按对象内容排序,那就要写自定义 comparator(解引用比较),而不是动默认 <

7. 今日迭代进度

✅ Day07 已完成:接口收尾 + pointer_cast + 比较/nullptr/swap 闭环(test_complete 跑通)

  • 别名构造(Aliasing Constructor)落地:成员指针/子对象视图安全可用
  • static/dynamic/const_pointer_cast 完整实现:全部共享控制块,不创建新对象
  • nullptr 支持与比较运算符补齐:并修正 operator!= 真值表坑
  • operator< 支持关联容器:按地址身份排序(std::less 思路)
  • Swap/Reset 可用,工程化层面命名与接口对齐更顺滑

如果你接下来还希望我把这篇再“更贴你的仓库真实命名/文件结构”(比如你到底叫 SharedPtr 还是 shared_ptr、控制块叫 SharedCount 还是 shared_count),你把 Day07 最终版的几个头文件片段贴出来(尤其是 my_shared_ptr.h / my_pointer_cast.h 的关键接口),我可以把第 3 节“关键实现”里的代码块完全替换成你仓库的真实版本(其余文字保持你这个口吻不变)。