C++每日训练 Day 12:仿 Qt 信号-槽机制的实现与实战应用

发布于:2025-04-17 ⋅ 阅读:(68) ⋅ 点赞:(0)

📘 本篇围绕 Qt 编程中最经典的设计之一——Signal/Slot(信号-槽)机制进行完整复现与讲解。从原理推导到实战构建,我们将结合 Day 10~11 的回调与观察者经验,逐步实现一个类型安全、生命周期安全、支持弱引用与自动解绑的轻量信号-槽系统,打通事件驱动设计能力。


🔁 Day 11 回顾:观察者模式构建要点

模块 内容总结
发布者-订阅者模型 通过模板与 function 实现注册/通知体系
weak_ptr 防悬空 使用智能指针确保观察者自动失效,不崩溃
事件 ID 注册机制 通过 Token 管理连接与解绑
模板化封装 支持任意参数传递、任意观察者结构

📌 本篇将在此基础上,引入 Slot(槽函数)+ Signal(信号发出者)模型,实现更自然、简洁的连接调用体验。


🎯 今日目标:构建轻量级 Signal/Slot 通信系统

组件 功能说明
Signal<T…> 提供 connect/disconnect/emit 接口
Slot / 回调 支持任意类型可调用对象,包括成员函数与 Lambda
生命周期安全 使用 weak_ptr 避免调用已销毁对象
高级支持 一次性连接、临时解绑、参数任意、嵌套安全调用

在这里插入图片描述

✅ 一、设计动机:Signal/Slot 比观察者更优在哪里?

🔍 特点对比:

功能维度 观察者模式 Signal/Slot
注册方式 通常使用注册函数(如 addObserver) 使用 connect 连接 signal 与 slot
调用触发 使用 notifyAll() signal.emit(…)
参数泛型支持 需手动泛型 模板内封装参数类型
支持成员方法 需配合 bind/std::function 可直接连接成员函数 + this
生命周期安全 weak_ptr + lock 内部管理 weak_ptr,自动判断失效

📌 Signal/Slot 更适合构建“模块间解耦事件响应机制”,特别是 UI 与业务层之间。


✅ 二、基本骨架设计:Signal 模板类

#include <functional>
#include <unordered_map>
#include <memory>
#include <iostream>

template<typename... Args>
class Signal {
public:
    using Slot = std::function<void(Args...)>;
    using Token = size_t;

    Token connect(Slot slot) {
        slots[++counter] = slot;
        return counter;
    }

    void disconnect(Token token) {
        slots.erase(token);
    }

    void emit(Args... args) {
        for (auto& [_, fn] : slots) {
            fn(args...);
        }
    }

private:
    std::unordered_map<Token, Slot> slots;
    Token counter = 0;
};

📌 接口说明:

  • connect(fn):注册一个槽函数,返回唯一连接标识
  • disconnect(id):根据 ID 解绑指定回调
  • emit(...):发送信号,自动调用所有注册函数

✅ 三、支持成员函数 + 弱引用绑定

🔸 辅助工具:bindWeakMember

template<typename T, typename... Args>
std::function<void(Args...)> bindWeakMember(std::weak_ptr<T> wp, void (T::*method)(Args...)) {
    return [wp, method](Args... args) {
        if (auto sp = wp.lock()) {
            (sp.get()->*method)(args...);
        }
    };
}

📌 用法:防止调用已销毁对象成员,自动跳过

🔸 示例结构:

struct Receiver : public std::enable_shared_from_this<Receiver> {
    void handle(int val) {
        std::cout << "📥 接收值:" << val << "\n";
    }
};

Signal<int> sig;

{
    auto r = std::make_shared<Receiver>();
    sig.connect(bindWeakMember(r, &Receiver::handle));
    sig.emit(100); // 正常输出
}

sig.emit(200); // 已销毁,自动跳过

✅ 四、拓展功能:一次性连接 / 自动解绑

🔸 一次性连接 once

template<typename... Args>
class SignalOnce : public Signal<Args...> {
public:
    typename Signal<Args...>::Token connectOnce(typename Signal<Args...>::Slot slot) {
        auto id = this->connect([this, slot, id = this->nextId()] (Args... args) mutable {
            slot(args...);
            this->disconnect(id);
        });
        return id;
    }

private:
    typename Signal<Args...>::Token nextId() { return ++this->counter; }
};

📌 适用场景:仅需一次响应的事件(如初始化完毕、一次确认)


✅ 五、完整应用案例:业务模块通信

📦 定义模块

class LoginManager {
public:
    Signal<std::string> onLoginSuccess;
    void login() {
        std::cout << "🔐 登录中...\n";
        onLoginSuccess.emit("user123");
    }
};

class ChatPanel : public std::enable_shared_from_this<ChatPanel> {
public:
    void onUserReady(const std::string& uid) {
        std::cout << "💬 欢迎用户:" << uid << " 进入聊天室\n";
    }
};

🤝 模块连接:

auto chat = std::make_shared<ChatPanel>();
LoginManager login;
login.onLoginSuccess.connect(bindWeakMember(chat, &ChatPanel::onUserReady));

login.login(); // 输出登录 & 聊天面板响应

✅ 六、经验总结

设计目标 实现方式
支持多回调 使用 map 储存 slots
生命周期安全 使用 weak_ptr + lock
支持任意参数函数 使用模板参数展开
支持解绑/一次性调用 提供 token + connectOnce 接口
支持成员方法绑定 使用 bindWeakMember 工具函数

📌 Signal/Slot 是观察者模式的模板化封装 + 生命周期控制的优雅实现方式。


📘 实战反思与扩展建议

  • 在 UI、游戏、客户端业务中非常适用
  • 可拓展支持事件分组、跨线程调用、异步触发
  • 可结合线程池、事件队列构建完整事件调度系统

🔭 Day 13 预告:协程 + 异步模型整合信号响应

下一篇将结合:

  • std::future / std::promise + Signal
  • 信号触发后自动 resume 协程流程
  • 构建“异步事件驱动业务模块”的基本框架

📌 如果你希望结合 GUI、游戏引擎、任务调度等真实项目场景,我可优先提供方向图与代码结构 💡


网站公告

今日签到

点亮在社区的每一天
去签到