第三章:实现数据源的生命周期管理 设计目标回顾 在前两章中,我们已经建立了Reaction框架的基础结构,本章将重点解决以下问题:
实现数据源的拷贝和移动语义
使用智能指针和引用计数管理内存
确保用户只持有弱引用,强引用由ObserverGraph统一管理
核心设计:桥接模式与智能指针管理 1. 桥接模式的应用 桥接模式(Bridge Pattern)是面向对象设计中的经典模式,其核心思想是将抽象(Abstraction)与实现(Implementation)分离,使二者可以独立变化。这种分离通过组合(Composition)而非继承(Inheritance)实现,是”组合优于继承”原则的典型体现。
在Reaction框架中,我们通过以下结构实现桥接模式:
1 2 3 4 5 6 template <typename ReactType>class React { };template <typename TriggerPolicy, typename InvalidStrategy, typename Type, typename ... Args>class ReactImpl : public Expression<TriggerPolicy, Type, Args...>, public InvalidStrategy { };
桥接模式的优势 :
接口(React
)与实现(ReactImpl
)可以独立变化
隐藏了复杂的模板参数和实现细节
用户只需与简洁的React
接口交互
2. 智能指针与引用计数 我们采用三级引用管理策略:
强引用 :由ObserverGraph
统一管理,确保活跃数据源不会被意外释放
弱引用 :用户持有的React
对象内部使用std::weak_ptr
弱引用计数 :ReactImpl
内部维护m_weakRefCount
,用于跟踪弱引用数量
1 2 3 4 5 6 7 8 9 class React { std::weak_ptr<ReactType> m_weakPtr; }; class ReactImpl { std::atomic<int > m_weakRefCount{0 }; };
实现关键点 1. 拷贝语义实现 拷贝构造和拷贝赋值需要正确处理弱引用计数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 React (const React &other) : m_weakPtr (other.m_weakPtr) { if (auto p = m_weakPtr.lock ()) p->addWeakRef (); } React &operator =(const React &other) noexcept { if (this != &other) { if (auto p = m_weakPtr.lock ()) p->releaseWeakRef (); m_weakPtr = other.m_weakPtr; if (auto p = m_weakPtr.lock ()) p->addWeakRef (); } return *this ; }
2. 移动语义实现 移动操作需要转移所有权并重置原对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 React (React &&other) noexcept : m_weakPtr (std::move (other.m_weakPtr)) { other.m_weakPtr.reset (); } React &operator =(React &&other) noexcept { if (this != &other) { if (auto p = m_weakPtr.lock ()) p->releaseWeakRef (); m_weakPtr = std::move (other.m_weakPtr); other.m_weakPtr.reset (); } return *this ; }
3. 弱引用计数管理 ReactImpl
需要提供弱引用计数的方法:
1 2 3 4 5 6 7 8 9 10 void addWeakRef () { m_weakRefCount++; } void releaseWeakRef () { if (--m_weakRefCount == 0 ) { ObserverGraph::getInstance ().removeNode (this ->shared_from_this ()); } }
Safe Bool Idiom(安全布尔惯用法)的历史 在 C++11 之前,C++ 没有直接支持安全的布尔转换机制,导致开发者需要设计各种技巧来防止隐式转换带来的问题。Safe Bool Idiom 就是在这个背景下诞生的,它的演进可以分为三个阶段:
1. 原始 operator bool
的问题(C++98/03) 在早期 C++ 中,如果一个类定义了 operator bool()
,它会允许 隐式转换 ,导致许多意外的行为:
1 2 3 4 5 6 7 8 9 class FileHandle {public : operator bool () const { return isValid (); } bool isValid () const ; }; FileHandle file; if (file) { }int x = file;
问题:
允许 FileHandle
隐式转换成 bool
,进而可能参与算术运算(如 file + 5
)。
可能与其他类型发生意外的隐式转换(如 if (file == nullptr)
可能编译通过,但逻辑错误)。
2. Safe Bool Idiom 的诞生(C++03 时代的解决方案) 为了避免隐式转换的问题,C++ 开发者发明了 Safe Bool Idiom ,主要思路是:
不直接返回 bool
,而是返回一个指向成员函数的指针 (通常是一个私有的、不可调用的函数)。
由于函数指针不能隐式转换成 int
或其他类型,但仍然可以在 if
、while
等布尔上下文中使用。
实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class FileHandle {public : typedef void (FileHandle::*SafeBoolType) () const ; operator SafeBoolType () const { return isValid () ? &FileHandle::dummy : nullptr ; } bool isValid () const ; private : void dummy () const {} }; FileHandle file; if (file) { }int x = file;
优点:
防止了隐式转换成 int
或其他类型。
仍然可以在布尔上下文中使用。
缺点:
代码复杂,引入了额外的成员函数和类型定义。
仍然有一些极端情况可能不安全(如 delete file
可能编译通过,但行为未定义)。
3. C++11 的 explicit operator bool
(现代解决方案) C++11 引入了 explicit
转换运算符 ,直接解决了 Safe Bool Idiom 的问题:
1 2 3 4 5 6 7 8 9 class FileHandle {public : explicit operator bool () const { return isValid (); } bool isValid () const ; }; FileHandle file; if (file) { }int x = file;
优点:
语法简洁,不需要额外的辅助类型。
完全防止隐式转换,只允许在布尔上下文中使用。
被标准库广泛采用(如 std::unique_ptr
、std::shared_ptr
都使用 explicit operator bool
)。
1 2 3 4 explicit operator bool () const { return !m_weakPtr.expired (); }
测试用例分析 让我们看看测试用例如何验证这些功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 TEST (ReactionTest, TestCopy) { auto a = reaction::var (1 ); auto b = reaction::var (3.14 ); auto ds = reaction::calc ([](int aa, double bb) { return std::to_string (aa) + std::to_string (bb); }, a, b); auto dds = reaction::calc ([](auto aa, auto dsds) { return std::to_string (aa) + dsds; }, a, ds); auto dds_copy = dds; EXPECT_EQ (dds_copy.get (), "113.140000" ); EXPECT_EQ (dds.get (), "113.140000" ); a.value (2 ); EXPECT_EQ (dds_copy.get (), "223.140000" ); EXPECT_EQ (dds.get (), "223.140000" ); } TEST (ReactionTest, TestMove) { auto a = reaction::var (1 ); auto b = reaction::var (3.14 ); auto ds = reaction::calc ([](int aa, double bb) { return std::to_string (aa) + std::to_string (bb); }, a, b); auto dds = reaction::calc ([](auto aa, auto dsds) { return std::to_string (aa) + dsds; }, a, ds); auto dds_copy = std::move (dds); EXPECT_EQ (dds_copy.get (), "113.140000" ); EXPECT_FALSE (static_cast <bool >(dds)); EXPECT_THROW (dds.get (), std::runtime_error); a.value (2 ); EXPECT_EQ (dds_copy.get (), "223.140000" ); EXPECT_FALSE (static_cast <bool >(dds)); }
实现注意事项
线程安全 :所有引用计数操作使用std::atomic
保证线程安全
异常安全 :移动操作标记为noexcept
,确保不会在转移所有权时抛出异常
空状态检查 :通过operator bool()
允许用户检查对象有效性
资源释放 :当弱引用计数归零时,通知ObserverGraph可以释放资源
总结 本章通过桥接模式和智能指针的巧妙结合,实现了:
安全的拷贝和移动语义
自动化的内存管理
清晰的接口与实现分离
高效的响应式更新机制