第三章:实现数据源的生命周期管理

设计目标回顾

在前两章中,我们已经建立了Reaction框架的基础结构,本章将重点解决以下问题:

  1. 实现数据源的拷贝和移动语义
  2. 使用智能指针和引用计数管理内存
  3. 确保用户只持有弱引用,强引用由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. 智能指针与引用计数

我们采用三级引用管理策略:

  1. 强引用:由ObserverGraph统一管理,确保活跃数据源不会被意外释放
  2. 弱引用:用户持有的React对象内部使用std::weak_ptr
  3. 弱引用计数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的拷贝构造函数
React(const React &other) : m_weakPtr(other.m_weakPtr) {
if (auto p = m_weakPtr.lock())
p->addWeakRef(); // 增加弱引用计数
}

// React的拷贝赋值运算符
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(React &&other) noexcept : m_weakPtr(std::move(other.m_weakPtr)) {
other.m_weakPtr.reset(); // 原对象放弃所有权
}

// React的移动赋值运算符
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) { /* OK */ }
int x = file; // 糟糕!file 被隐式转换成 bool,再转成 int(可能非预期)

问题:

  • 允许 FileHandle 隐式转换成 bool,进而可能参与算术运算(如 file + 5)。
  • 可能与其他类型发生意外的隐式转换(如 if (file == nullptr) 可能编译通过,但逻辑错误)。

2. Safe Bool Idiom 的诞生(C++03 时代的解决方案)

为了避免隐式转换的问题,C++ 开发者发明了 Safe Bool Idiom,主要思路是:

  • 不直接返回 bool,而是返回一个指向成员函数的指针(通常是一个私有的、不可调用的函数)。
  • 由于函数指针不能隐式转换成 int 或其他类型,但仍然可以在 ifwhile 等布尔上下文中使用。

实现方式:

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) { /* OK */ }
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) { /* OK */ }
int x = file; // 编译错误!必须显式转换

优点:

  • 语法简洁,不需要额外的辅助类型。
  • 完全防止隐式转换,只允许在布尔上下文中使用。
  • 被标准库广泛采用(如 std::unique_ptrstd::shared_ptr 都使用 explicit operator bool)。
1
2
3
4
explicit operator bool() const {
return !m_weakPtr.expired(); // Returns true if the pointer is not null
}

测试用例分析

让我们看看测试用例如何验证这些功能:

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 for moving data sources
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));
}

实现注意事项

  1. 线程安全:所有引用计数操作使用std::atomic保证线程安全
  2. 异常安全:移动操作标记为noexcept,确保不会在转移所有权时抛出异常
  3. 空状态检查:通过operator bool()允许用户检查对象有效性
  4. 资源释放:当弱引用计数归零时,通知ObserverGraph可以释放资源

总结

本章通过桥接模式和智能指针的巧妙结合,实现了:

  1. 安全的拷贝和移动语义
  2. 自动化的内存管理
  3. 清晰的接口与实现分离
  4. 高效的响应式更新机制