目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day07 我主要干一件事:把 shared_ptr/weak_ptr 的“对外接口”补齐到能像 Boost/标准库一样顺滑使用。今天我重点对齐了 Boost 的几个关键设计:别名构造(Aliasing Constructor) 、三种 *_pointer_cast、nullptr 支持、比较运算符与 swap/reset,再做一轮工程化收尾(命名一致、坏味道处理)。核心理解也很统一:共享同一个控制块,不创建新对象。
1. 今日目标
在 Day06 已经跑通 make_shared(inplace 控制块 + placement new)之后,Day07 我把精力放在“接口闭环”上:
- 补齐 shared_ptr/weak_ptr 的收尾接口:
Reset/Swap、判空(operator bool/== nullptr/!= nullptr)、跨类型相等比较等。 - 实现类型转换工具:
static_pointer_cast / dynamic_pointer_cast / const_pointer_cast,并保证转换后共享控制块。 - 工程化收尾:整理头文件依赖、把命名统一(避免
shared_ptr/SharedPtr混用)、把一些容易误写的点(比如nullptr_t、operator!=真值表)彻底对齐。
核心收获:我现在能清楚地解释——
pointer_cast 并不是“创建新对象”,而是“创建一个转换后的 shared_ptr 句柄”,并且它和原 shared_ptr 共享同一个控制块。
2. 今日设计:别名构造与 pointer_cast 的统一模型
2.1 别名构造(Aliasing Constructor)到底解决什么?
我今天最关键的认知是:shared_ptr 里保存的指针(ptr)和“拥有者”(控制块)可以不是同一个东西。
别名构造的语义我用一句话记住:
我让 shared_ptr 的
get()指向一个“子对象/成员指针 p”,但它的生命周期跟随另一个 owner shared_ptr 的控制块。
举个我自己最常用的场景:
我想拿 Person 的 name 成员指针,但我必须保证 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 指针 + 别名构造
我把它们统一成一个模式来理解:
- 从
r.get()拿到裸指针 - 做一次 cast 得到
p - 用 别名构造
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 == nullptr 用 return !p;,然后写 != 时复制粘贴也写成 return !p;,结果 != 的语义就反了。
正确真值表我强制记住:
p == nullptr⇔!pp != nullptr⇔bool(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 节“关键实现”里的代码块完全替换成你仓库的真实版本(其余文字保持你这个口吻不变)。