C++信号与槽机制自实现:从Qt原理到现代C++事件通信库

📅 2026/7/16 5:46:08 👁️ 阅读次数 📝 编程学习
C++信号与槽机制自实现:从Qt原理到现代C++事件通信库

1. 项目概述:为什么我们需要自己实现信号与槽?

在C++ GUI编程,尤其是Qt框架的日常开发中,信号与槽机制是我们再熟悉不过的“基础设施”。它优雅地解决了对象间通信的问题,让一个按钮的点击能自动触发一个窗口的关闭,或者让一个进度条的值变化实时更新到标签上。然而,当我们习惯了Qt提供的QObject::connectemitsignalsslots这些关键字时,是否曾想过,如果脱离Qt,我们能否在纯C++项目中复现这种松耦合、类型安全的通信机制?这就是“C++信号与槽机制自实现”项目的核心价值。

这个项目绝不仅仅是“重新发明轮子”。它的意义在于深度理解。通过亲手构建一套信号与槽系统,你将彻底吃透其背后的设计思想:元对象系统如何工作?连接(Connection)如何存储和管理?线程安全如何保证?类型安全又是如何实现的?这远比单纯调用connect函数要深刻得多。对于希望深入C++元编程、设计模式以及高性能事件驱动架构的开发者来说,这是一个绝佳的练手项目。它剥离了Qt的庞大生态,让你聚焦于核心机制,最终产出的将是一个轻量级、可嵌入任何C++项目的通用事件通信库。

2. 核心设计思路:从Qt中汲取灵感

Qt的信号与槽机制之所以强大,在于它结合了元对象系统(Meta-Object System)模板元编程。我们的自实现版本无法(也不必要)完全复刻Qt的MOC(元对象编译器)预处理器,但可以借鉴其核心思想,用现代C++(C++11/14/17)的特性来实现一个简化但功能完备的版本。

2.1 核心目标拆解

我们的自实现信号与槽系统需要满足以下几个核心目标:

  1. 类型安全:连接信号和槽时,编译器应能检查参数类型是否匹配,避免运行时错误。这是优于传统函数指针和std::function<void(void*)>方案的关键。
  2. 松耦合:信号的发送者完全不知道也不关心有哪些槽函数接收它。槽函数也不知道信号来自何处。
  3. 一对多与多对一:一个信号可以连接多个槽,一个槽也可以接收多个信号。
  4. 自动连接管理:当信号发送者或槽函数所属对象被销毁时,相关的连接应能自动断开,防止悬空调用。
  5. 线程安全(可选但重要):支持跨线程的信号发射与槽执行,这是现代应用的基础需求。

2.2 关键技术选型与原理

为了实现上述目标,我们需要组合运用多种C++高级特性:

  • std::functionstd::bind/ Lambda:这是实现可调用对象封装的基础。一个“槽”本质上就是一个std::function对象。信号则需要维护一个std::function的列表。
  • 变参模板(Variadic Templates):这是实现类型安全的核心。我们需要定义一个模板类Signal,其模板参数就是信号的参数列表(如Signal<int, std::string>)。这样,在编译期就能确定信号和槽的函数签名。
  • 智能指针与弱引用:为了实现自动连接管理,我们需要跟踪信号源和槽所属对象的生命周期。通常使用std::shared_ptrstd::weak_ptr。连接(Connection)可以持有槽对象所属实例的std::weak_ptr。当尝试调用槽时,先检查weak_ptr是否有效(对象是否存活)。
  • 函数签名擦除与类型擦除:虽然我们使用模板来保证类型安全,但在存储多个不同类型的槽时(例如,一个Signal<int>连接了多个不同的函数对象),我们需要一种统一的方式来存储它们。这可以通过定义一个非模板的基类ISlot,然后由模板派生类SlotImpl来保存具体的std::function和类型信息。

3. 核心类设计与实现细节

接下来,我们一步步构建核心类。为了清晰,我们将创建一个命名空间MyQt

3.1 连接句柄(Connection)与可调用对象封装(SlotBase)

首先,我们需要一个代表“连接”的轻量级对象,用于在需要时断开连接。

// connection.h #pragma once #include <memory> namespace MyQt { class Connection { public: Connection() = default; virtual ~Connection() = default; // 禁用拷贝,允许移动 Connection(const Connection&) = delete; Connection& operator=(const Connection&) = delete; Connection(Connection&&) = default; Connection& operator=(Connection&&) = default; // 断开此连接 virtual void disconnect() = 0; // 检查连接是否仍有效(例如,对应的槽对象是否已被销毁) virtual bool isConnected() const = 0; }; using ConnectionPtr = std::shared_ptr<Connection>; }

然后,定义一个槽的抽象基类。它不关心具体的函数签名,只负责提供调用和生命周期查询接口。

// slot_base.h #pragma once #include <memory> #include “connection.h” namespace MyQt { class Object; // 前向声明,代表拥有槽的对象 class SlotBase : public std::enable_shared_from_this<SlotBase> { public: virtual ~SlotBase() = default; // 获取与此槽关联的对象(用于生命周期管理) virtual std::weak_ptr<Object> getOwner() const = 0; // 一个标记接口,具体调用由派生类实现 }; using SlotBasePtr = std::shared_ptr<SlotBase>; }

3.2 信号类(Signal)的实现

这是最核心的部分。我们将实现一个模板类Signal,它管理一个SlotBase的列表,并提供一个emit(或operator())方法来触发所有已连接的槽。

// signal.h #pragma once #include <vector> #include <memory> #include <algorithm> #include “slot_base.h” #include “connection.h” namespace MyQt { // 前向声明 class Object; template <typename... Args> class Signal { public: Signal() = default; ~Signal() { // 信号析构时,应断开所有连接,但通常由Connection对象自己管理 } // 连接一个槽。槽可以是任何可调用对象(函数、lambda、成员函数等) template <typename Func> ConnectionPtr connect(Func&& func) { // 这里需要一个机制将func包装成SlotBasePtr // 我们稍后实现一个SlotImpl auto slot = std::make_shared<SlotImpl<Func>>(std::forward<Func>(func)); slots_.push_back(slot); // 创建一个Connection对象,其disconnect()操作需要能从slots_中移除自身 auto conn = std::make_shared<ConnectionImpl>(this, slot); slot->setConnection(conn); return conn; } // 连接一个对象的成员函数,这是更常见的用法 template <typename T, typename Method> ConnectionPtr connect(std::shared_ptr<T> obj, Method&& method) { // 使用std::bind或lambda将成员函数绑定到对象实例 auto func = [obj, method](Args... args) { // 调用成员函数 // 注意:这里需要检查obj是否有效,但lambda捕获的shared_ptr保证了对象存在性 // 更安全的做法是捕获weak_ptr,在调用前lock() }; return connect(std::move(func)); } // 发射信号,触发所有连接的槽 void emit(Args... args) const { // 注意:在遍历过程中,槽的调用可能会修改slots_列表(例如断开连接) // 因此需要先复制一份列表,或者使用更安全的方式遍历 auto slotsCopy = slots_; for (const auto& slot : slotsCopy) { // 这里需要将slot向下转型为具体的SlotImpl<...>并调用 // 我们先调用一个虚函数接口 if (auto slotPtr = slot.lock()) { // 动态调用,需要类型擦除和参数转发,这是难点 // 我们将在SlotImpl中实现一个纯虚函数 `call(Args...)` // slotPtr->call(args...); } } } // 仿函数形式发射信号,更符合C++习惯 void operator()(Args... args) const { emit(std::forward<Args>(args)...); } // 断开所有连接 void disconnectAll() { slots_.clear(); } private: // 存储槽的弱引用,避免槽持有信号的引用导致循环引用? // 实际上,通常存储的是shared_ptr,因为Connection需要管理生命周期。 // 但为了自动清理失效连接,更佳实践是存储weak_ptr<SlotBase>, // 并在emit时过滤掉已失效的。 std::vector<std::weak_ptr<SlotBase>> slots_; // Connection的实现类,需要访问Signal的私有成员以从slots_中移除自己 class ConnectionImpl : public Connection { public: ConnectionImpl(Signal* sig, std::shared_ptr<SlotBase> slot) : signal_(sig), slot_(slot) {} void disconnect() override { if (signal_) { signal_->removeSlot(slot_); signal_ = nullptr; slot_.reset(); } } bool isConnected() const override { return signal_ != nullptr && !slot_.expired(); } private: Signal* signal_; std::weak_ptr<SlotBase> slot_; }; // 从列表中移除特定的槽 void removeSlot(const std::shared_ptr<SlotBase>& slot) { slots_.erase( std::remove_if(slots_.begin(), slots_.end(), [&slot](const std::weak_ptr<SlotBase>& wp) { auto sp = wp.lock(); return !sp || sp == slot; }), slots_.end()); } }; }

上面的代码勾勒了框架,但最关键的类型安全的调用slotPtr->call(args...)还没有实现。这需要我们在SlotBase中引入一个带变参的纯虚函数,但C++不允许虚函数模板。因此,我们需要一种类型擦除技术。

3.3 类型擦除的槽实现(SlotImpl)

为了解决上述问题,我们引入一个中间层:SlotInvokerSignalemit时,并不直接调用SlotBase,而是通过一个SlotInvoker来转发参数,这个Invoker知道具体的参数类型。

// slot_impl.h #pragma once #include “slot_base.h” #include <functional> #include <memory> namespace MyQt { // 一个辅助的调用器基类,它知道如何用一组特定的参数调用某个东西 template <typename... Args> class SlotInvokerBase { public: virtual ~SlotInvokerBase() = default; virtual void invoke(Args... args) = 0; }; // 具体的调用器,持有一个std::function template <typename Func, typename... Args> class SlotInvoker : public SlotInvokerBase<Args...> { public: explicit SlotInvoker(Func func) : func_(std::move(func)) {} void invoke(Args... args) override { func_(std::forward<Args>(args)...); } private: Func func_; }; // 槽的实现类,它组合了一个SlotInvoker class SlotImpl final : public SlotBase { public: template <typename Func, typename... Args> SlotImpl(Func&& func) { // 关键:创建一个知道参数类型(Args...)的invoker // 但是,SlotImpl本身并不知道Args...是什么,它需要被存储为类型擦除的形式。 // 这引出了下一个设计:我们需要一个非模板的基类来存储invoker,但invoker本身是模板化的。 // 一个常见的做法是使用 `std::function<void(Args...)>`,但Args在SlotImpl里是未知的。 // 因此,我们需要将invoker的存储也类型擦除。 } // ... 其他接口 }; }

看来我们遇到了一个核心挑战:如何在非模板的SlotBase中,存储一个能够以类型安全的方式调用任意函数签名的调用器?

解决方案是使用双重类型擦除。我们定义一个新的基类AnyInvoker,它只有一个纯虚函数invoke(void*)(或类似的泛型接口)。然后,由TypedInvoker<Args...>继承它,并在内部将void*参数转换回具体的Args...并调用真正的std::function。这需要一些技巧,因为参数打包和拆包需要内存操作。

一个更简洁、利用C++17std::anystd::apply的现代实现思路如下:

  1. Signal<Args...>内部存储一个std::vector<std::function<void(Args...)>>。这直接保证了类型安全。
  2. 当连接一个泛型可调用对象时,使用std::function<void(Args...)>进行包装。如果类型不匹配,会在编译时(std::function构造时)报错。
  3. 为了支持成员函数和自动生命周期管理,connect函数需要更智能的包装。

让我们调整设计,采用更直接的方案:

最终版Signal核心实现:

// signal.h (修订版) #pragma once #include <functional> #include <vector> #include <memory> #include <algorithm> #include <mutex> // 用于线程安全 namespace MyQt { template <typename... Args> class Signal { using SlotFunction = std::function<void(Args...)>; public: Signal() = default; // 连接一个自由函数、静态函数或lambda template <typename Func> std::shared_ptr<void> connect(Func&& func) { std::lock_guard<std::mutex> lock(mutex_); slots_.emplace_back(std::forward<Func>(func)); // 返回一个标记,用于断开连接。这里简单返回slot的迭代器或ID。 // 更健壮的做法是返回一个Connection对象。 // 我们返回一个指向该function在vector中位置的“令牌”。 // 但由于vector可能重分配,这不是好主意。改用shared_ptr<int>作为令牌。 auto token = std::make_shared<int>(slots_.size() - 1); // 我们需要建立token到slot的映射,以便disconnect。这里简化处理,在disconnect时遍历查找。 // 更好的结构是 std::list + std::shared_ptr 直接管理。 return token; } // 连接一个对象的成员函数,并自动管理对象生命周期 template <typename T, typename... U> std::shared_ptr<void> connect(std::shared_ptr<T> obj, void (T::*method)(U...)) { // 静态断言检查参数包U...是否与Args...兼容(相同或可转换) // 这里简化,假设 U... 就是 Args... static_assert(std::is_same<std::tuple<Args...>, std::tuple<U...>>::value, “Signal and slot argument types must match exactly for member functions.”); // 捕获对象的weak_ptr,避免循环引用,并在调用前检查对象是否存活 std::weak_ptr<T> weakObj = obj; auto slotFunc = [weakObj, method](Args... args) { if (auto sharedObj = weakObj.lock()) { // 调用成员函数 (sharedObj.get()->*method)(args...); } // 如果对象已销毁,则什么也不做(相当于自动断开连接) }; return connect(std::move(slotFunc)); } // 发射信号 void emit(Args... args) const { std::lock_guard<std::mutex> lock(mutex_); // 遍历并调用所有槽函数 for (const auto& slot : slots_) { if (slot) { // 检查function是否有效(可能被disconnect置空) slot(args...); } } // 清理无效的slot(被置空的) // 注意:在const成员函数中修改mutable成员是允许的 const_cast<Signal*>(this)->cleanupSlots(); } void operator()(Args... args) const { emit(std::forward<Args>(args)...); } // 断开连接(通过token) void disconnect(const std::shared_ptr<void>& token) { std::lock_guard<std::mutex> lock(mutex_); // 我们需要找到这个token对应的slot。由于我们之前简化了,没有建立映射。 // 因此,这个简化版的disconnect功能有限。一个完整的实现需要Connection类来维护反向引用。 // 这里我们采用另一种常见策略:将slot替换为一个空function(标记为失效)。 // token可以是一个指向std::function的shared_ptr的weak_ptr。 } // 断开所有连接 void disconnectAll() { std::lock_guard<std::mutex> lock(mutex_); slots_.clear(); } private: mutable std::mutex mutex_; // mutable允许在const emit中加锁 std::vector<SlotFunction> slots_; void cleanupSlots() { slots_.erase( std::remove_if(slots_.begin(), slots_.end(), [](const SlotFunction& sf) { return !sf; }), // 移除无效的function slots_.end()); } }; } // namespace MyQt

这个简化版本已经可以实现核心功能,但缺少健壮的Connection对象来管理生命周期。让我们补全它。

4. 完整实现:带Connection管理的线程安全信号

我们将实现一个更完整的版本,包含ScopedConnection(RAII连接)和线程安全。

4.1 Connection对象的完整实现

// connection.h #pragma once #include <memory> #include <functional> namespace MyQt { class Connection { public: using Disconnector = std::function<void()>; Connection() = default; explicit Connection(Disconnector&& disconnector) : disconnector_(std::move(disconnector)) {} virtual ~Connection() { disconnect(); } void disconnect() { if (disconnector_) { disconnector_(); disconnector_ = nullptr; // 防止重复调用 } connected_ = false; } bool isConnected() const { return connected_; } // 允许移动 Connection(Connection&& other) noexcept : disconnector_(std::move(other.disconnector_)), connected_(other.connected_) { other.connected_ = false; } Connection& operator=(Connection&& other) noexcept { if (this != &other) { disconnect(); disconnector_ = std::move(other.disconnector_); connected_ = other.connected_; other.connected_ = false; } return *this; } // 禁用拷贝 Connection(const Connection&) = delete; Connection& operator=(const Connection&) = delete; private: Disconnector disconnector_; bool connected_{true}; }; // RAII风格的连接,超出作用域自动断开 class ScopedConnection { public: ScopedConnection() = default; explicit ScopedConnection(Connection&& conn) : conn_(std::move(conn)) {} ~ScopedConnection() { if (conn_.isConnected()) conn_.disconnect(); } // 可以接管一个Connection的所有权 ScopedConnection& operator=(Connection&& conn) { if (conn_.isConnected()) conn_.disconnect(); conn_ = std::move(conn); return *this; } void disconnect() { conn_.disconnect(); } bool isConnected() const { return conn_.isConnected(); } private: Connection conn_; }; } // namespace MyQt

4.2 最终版Signal实现(带Connection管理)

// signal.h #pragma once #include “connection.h” #include <functional> #include <vector> #include <memory> #include <mutex> #include <algorithm> namespace MyQt { template <typename... Args> class Signal { public: using SlotType = std::function<void(Args...)>; Signal() = default; ~Signal() { // 信号析构时,断开所有连接 disconnectAll(); } // 连接一个可调用对象,返回Connection用于管理生命周期 template <typename Func> Connection connect(Func&& func) { std::lock_guard<std::mutex> lock(mutex_); // 分配一个唯一的ID(可以用slots_的索引,但删除后索引会变,所以用size_t递增) size_t id = nextId_++; slots_.emplace_back(SlotInfo{id, std::make_shared<SlotType>(std::forward<Func>(func))}); // 返回一个Connection,其disconnector会从slots_中移除这个slot return Connection([this, id]() { std::lock_guard<std::mutex> lock(mutex_); auto it = std::find_if(slots_.begin(), slots_.end(), [id](const SlotInfo& info) { return info.id == id; }); if (it != slots_.end()) { // 不直接erase,而是标记为nullptr,在emit时清理,避免迭代器失效 it->slot.reset(); } }); } // 连接成员函数(支持智能指针管理对象生命周期) template <typename T, typename... U> Connection connect(std::shared_ptr<T> obj, void (T::*method)(U...)) { // 使用std::is_invocable进行更严格的编译期检查 static_assert(std::is_invocable<decltype(method), T*, U...>::value, “Member function signature mismatch.”); // 确保U...可转换为Args... // 这里简化,假设完全匹配。实际可以使用更复杂的类型traits。 std::weak_ptr<T> weakObj = obj; auto slotFunc = [weakObj, method](Args... args) { if (auto sharedObj = weakObj.lock()) { // 使用std::invoke以统一语法调用成员函数 std::invoke(method, sharedObj.get(), args...); } }; return connect(std::move(slotFunc)); } // 发射信号 void emit(Args... args) const { std::vector<std::shared_ptr<SlotType>> validSlots; { std::lock_guard<std::mutex> lock(mutex_); // 收集所有有效的slot for (const auto& info : slots_) { if (info.slot && *info.slot) { validSlots.push_back(info.slot); } } // 清理无效的slot(被disconnect标记的) cleanupSlots(); } // 在锁外调用槽函数,避免死锁(槽函数内部可能再次操作本信号) for (const auto& slot : validSlots) { (*slot)(args...); } } void operator()(Args... args) const { emit(std::forward<Args>(args)...); } // 断开所有连接 void disconnectAll() { std::lock_guard<std::mutex> lock(mutex_); slots_.clear(); nextId_ = 0; } // 获取当前连接数(主要用于调试) size_t connectionCount() const { std::lock_guard<std::mutex> lock(mutex_); return std::count_if(slots_.begin(), slots_.end(), [](const SlotInfo& info) { return info.slot && *info.slot; }); } private: struct SlotInfo { size_t id; std::shared_ptr<SlotType> slot; // 使用shared_ptr便于外部置空来标记断开 }; mutable std::mutex mutex_; std::vector<SlotInfo> slots_; size_t nextId_{0}; void cleanupSlots() { slots_.erase( std::remove_if(slots_.begin(), slots_.end(), [](const SlotInfo& info) { return !info.slot || !(*info.slot); }), slots_.end()); } }; } // namespace MyQt

4.3 使用示例

现在,我们可以像使用Qt信号槽一样使用我们的自实现版本了。

#include “signal.h” #include <iostream> #include <memory> class Button : public std::enable_shared_from_this<Button> { public: MyQt::Signal<> clicked; // 无参信号 void simulateClick() { std::cout << “Button clicked.” << std::endl; clicked.emit(); // 发射信号 } }; class Window : public std::enable_shared_from_this<Window> { public: void onButtonClicked() { std::cout << “Window handles button click.” << std::endl; } void show() { std::cout << “Window shown.” << std::endl; } }; int main() { auto button = std::make_shared<Button>(); auto window = std::make_shared<Window>(); // 连接信号和槽(成员函数) auto conn1 = button->clicked.connect(window, &Window::onButtonClicked); // 连接信号和槽(Lambda表达式) auto conn2 = button->clicked.connect([]() { std::cout << “Lambda slot called.” << std::endl; }); // 模拟点击 button->simulateClick(); // 输出: // Button clicked. // Window handles button click. // Lambda slot called. // 断开一个连接 conn2.disconnect(); button->simulateClick(); // 输出: // Button clicked. // Window handles button click. // 使用ScopedConnection,超出作用域自动断开 { MyQt::ScopedConnection scopedConn = button->clicked.connect([]() { std::cout << “This connection will auto-dispose.” << std::endl; }); button->simulateClick(); // 会调用这个临时槽 } // scopedConn析构,自动断开 button->simulateClick(); // 不会调用已断开的槽 // 信号析构时,所有连接自动断开 return 0; }

5. 关键问题与优化策略

在实际使用中,你会遇到一些Qt已经解决但我们需要自己处理的问题。

5.1 线程安全与事件队列

我们的基础实现通过std::mutex保证了slots_容器的线程安全,但emit是同步调用。在GUI或异步编程中,我们常常需要跨线程发射信号,并让槽在目标对象所在的线程(例如主线程)中执行。这需要引入事件队列线程上下文的概念。

解决方案:可以扩展Signal,在connect时增加一个参数指定连接类型(如DirectConnection同步,QueuedConnection异步)。对于队列连接,emit时将调用请求包装成一个std::function,投递到目标线程的事件队列中。这需要与一个全局或线程局部的事件循环(EventLoop)配合。

enum class ConnectionType { Direct, // 直接调用(发射线程) Queued // 队列调用(目标对象所在线程) }; template <typename... Args> Connection Signal<Args...>::connect(std::shared_ptr<Object> context, Func&& func, ConnectionType type) { if (type == ConnectionType::Queued) { // 包装func,使其通过context对象的事件队列调用 auto queuedFunc = [context, func = std::forward<Func>(func)](Args... args) { if (auto ctx = context.lock()) { ctx->postEvent([func, args...]() { func(args...); }); } }; return connect(std::move(queuedFunc)); } return connect(std::forward<Func>(func)); }

5.2 性能考量

  • 锁粒度:我们的实现在每次connectemitdisconnect时都锁住了整个slots_向量。在高频信号场景下,这可能成为瓶颈。可以考虑使用读写锁(std::shared_mutex,C++17)或更细粒度的数据结构,如每个槽自带一个原子标志位来表示是否有效。
  • 参数传递emit时参数被复制给每个槽。如果参数是大型对象,成本很高。可以考虑使用完美转发和引用,但要注意生命周期。一种常见模式是信号参数使用const T&或值语义,由调用者保证参数在槽执行期间有效。
  • 动态分配:每次connect都涉及动态内存分配(std::functionSlotInfo)。对于性能极其苛刻的场景,可以考虑使用小型缓冲区优化(SBO)的function实现,或对象池。

5.3 与Qt信号的差异

我们的自实现与Qt信号相比,主要简化或缺失了以下特性:

  1. 元对象系统:我们没有实现moc那样的预编译步骤,因此不支持signalsslotsemit关键字。我们使用普通的成员变量(Signal对象)和函数调用。
  2. 自动连接语法:Qt的connect可以使用字符串形式的信号和槽名,在运行时通过元对象系统查找。我们只支持编译时类型安全的函数指针/std::function连接。
  3. 信号重载:Qt可以区分重载的信号,如QComboBox::currentIndexChanged(int)QComboBox::currentIndexChanged(const QString&)。我们的模板类Signal<Args...>本身通过不同的参数列表就是不同的类型,天然支持“重载”,但需要定义多个不同签名的Signal成员变量。
  4. 信号连接信号:我们的Signal::emit返回void,而Qt的信号可以连接另一个信号。要实现这个,可以让emit返回一个代表此次发射的可调用对象,或者让Signal本身也是一个可连接的对象。

5.4 一个实用的改进:Signal作为成员变量

为了让类定义更清晰,可以定义一个宏来简化信号的声明,类似于Qt的Q_SIGNALS(但功能弱很多)。

// my_signal_macros.h #pragma once #define MY_SIGNAL(name, ...) \ public: \ MyQt::Signal<__VA_ARGS__> name; \ private: // 使用示例 class MyWidget { MY_SIGNAL(valueChanged, int, const std::string&) MY_SIGNAL(clicked) public: void setValue(int v, const std::string& unit) { valueChanged.emit(v, unit); } };

这个宏只是语法糖,将信号声明为公有成员变量,并确保其后的成员是私有的(模仿Qt的signals:区段效果)。实际项目中可根据需要调整。

通过这个从零开始实现信号与槽机制的过程,你不仅获得了一个可用的轻量级库,更重要的是深入理解了观察者模式、C++模板元编程、类型擦除、线程安全以及资源生命周期管理等核心概念。这远比仅仅阅读Qt源码或使用Qt来得深刻。下次当你再写下connect时,你会对屏幕背后发生的一切了如指掌。