欢迎光临
我们一直在努力

C++编程实践—C++实现类似Qt的信号槽机制

一、说明

学习一个技术的最经典的方式是什么?就是重复造轮子。大到国家小到个人,基本都是这样,先引进消化,吸收后才能够慢慢进行创新。所谓引进消化,其实就是自己造轮子,只有造好轮子,真正掌握其中的技术原理和细节,才能够“知其所以然”。
在前面分析了很多有名气的开源软件框架的源码,其实也是这个意思。从底层掌握设计、开发的全流程,然后才能开始用其优秀之处进行自己的实际应用。在实际中融会贯通的理解优秀的底层实现逻辑后,才能够据此进行更多更好的创新。

二、Qt的信号-槽机制

Qt作为一个在C++中的优秀平台,广泛应用于GUI和跨平台的开发。在Qt中,其基础的信号和槽机制是GUI开发的重要一环。信号-槽机制本质是一种实现了对类型安全、对象生命周期管理、跨线程派发的观察者机制,其表现形式仍然是函数。在Qt中,为了实现的方便,提供了MOC的元编程机制(Q_OBJECT);为了匹配的适应性,也提供了多种的连接类型。Qt提供了对信号槽机制的安全保证即生命周期的安全控制管理。在跨线程应用时,只要小心使用,也是没有大问题的。
需要注意的是,在新的支持Lambda表达式的机制中,要小心生命周期的不匹配。信号机制决定了它在跨线程高频使用时可能会遇到一些问题,如队列的堆积、处理的效率等。特别是大数据的传递,最好采用指针或共享数据(可能需要锁)。
那么Qt中的信号和槽到底是什么呢?信号和槽,Signals and Slots。是一种高级的回调机制,它提供了对象之间的通信方式并提高了代码的可读性和维护性。信号和槽其实誻一个发送者对象(Sender)通过发射(Emitting)一个信号(Signal)来通知另一个接收者对象(Receiver)某个事件的发生。它们之间通过槽(Slot)来实现对事件的响应。
其基本的流程如下:

  • 声明信号
    即声明信号函数,一般如下:

    signals:
    void dataChanged(int d);

  • 声明槽
    编写槽的函数,一般如下:

    public slots:
    void onDataChanged(int d);

  • 建立连接
    将信号和槽建立联系,如下方法:

    connect(sender, &Sender::dataChanged,
    receiver, &Receiver::onDataChanged);

  • 发送信号
    真正的触发事件,其方式基本为:

    emit valueChanged(1);

  • 当然在细节实现上,还有很多的相关技术点,如MOC机制、反射及跨线程应用等等,有兴趣可以自行查看,但此处不是重点。

    三、C++的实现

    有Qt的实现在前,那么如果用C++自己怎么实现呢?首先要想一下Qt实现的方式,自己如何才能够与其达到一致。

  • 创建函数以及函数的传递需要函数对象std::function
  • 信号和槽函数的处理需要std::bind
  • 参数转发处理可能需要std::forward
  • 参数及对象控制需要智能指针
  • 如果再复杂一些需要支持Lambda表达式
  • 模板技术特别是变参模板
  • 这可以算是一个相对简单实现的流程,它不需要MOC和反射,也不需要支持连接类型等,只从原理上实现相关的信号槽机制。明白了相关实现需要的技术,下面看一个最基础的实现:

    #include <iostream>
    #include <vector>
    #include <functional>

    class Signal {
    public:
    void connect(std::function<void()> sf) {
    slotFuncs.push_back(sf);
    }

    void emit() {
    for (auto& sf : slotFuncs) sf();
    }

    private:
    std::vector<std::function<void()>> slotFuncs;
    };

    int main() {
    Signal btnClick;

    btnClick.connect([]() { std::cout << "call first slot function!"<<std::endl; });
    btnClick.connect([]() {
    std::cout << "call second slot function!"<<std::endl; });

    btnClick.emit();
    }

    看到上面的代码会不会想用一个map存储槽对象,传参怎么办?如何处理变参?是不是和刚刚提到的技术应用对应上了。

    四、例程

    上面的基础实现,其实是提供了一个复刻Qt信号槽机制的入门方式,即造一个长得有点像的最简单轮子。下面再看一个相对复杂的实现:

    #include <functional>
    #include <iostream>
    #include <mutex>
    #include <string>
    #include <unordered_map>
    #include <vector>

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

    size_t connect(Slot slot) {
    std::lock_guard<std::mutex> lock(mutex_);
    size_t id = nextId_++;
    slots_[id] = std::move(slot);
    return id;
    }

    template <typename T> size_t connect(T *obj, void (T::*method)(Args...)) {
    return connect([obj, method](Args... args) { (obj->*method)(args...); });
    }

    void disconnect(size_t id) {
    std::lock_guard<std::mutex> lock(mutex_);
    slots_.erase(id);
    }

    void emit(Args... args) {
    std::vector<Slot> copiedSlots;

    {
    std::lock_guard<std::mutex> lock(mutex_);
    for (auto &[id, slot] : slots_) {
    copiedSlots.push_back(slot);
    }
    }

    for (auto &slot : copiedSlots) {
    slot(args...);
    }
    }

    void operator()(Args... args) { emit(args...); }

    private:
    std::mutex mutex_;
    std::unordered_map<size_t, Slot> slots_;
    size_t nextId_ = 1;
    };

    class Button {
    public:
    Signal<> clicked;

    void click() {
    std::cout << "button clicked" << std::endl;
    clicked.emit();
    }
    };

    class Slider {
    public:
    Signal<int> valueChanged;

    void setValue(int v) {
    if (value_ == v) {
    return;
    }

    value_ = v;
    valueChanged.emit(value_);
    }

    private:
    int value_ = 0;
    };

    class Label {
    public:
    void setTextFromClick() { std::cout << "set lbael clicked" << std::endl; }

    void setNumber(int value) { std::cout << " value = " << value << std::endl; }
    };

    int main() {
    Button btn;
    Slider slider;
    Label lab;

    auto connClick = btn.clicked.connect(&lab, &Label::setTextFromClick);

    auto connSet = slider.valueChanged.connect(&lab, &Label::setNumber);

    auto connLambda = slider.valueChanged.connect([](int value) {
    std::cout << "slot received value: " << value << std::endl;
    });

    btn.click();

    slider.setValue(11);
    slider.setValue(22);

    std::cout << "disconnect lambda slot" << std::endl;
    slider.valueChanged.disconnect(connLambda);

    slider.setValue(23);

    std::cout << "disconnect button slot" << std::endl;
    btn.clicked.disconnect(connClick);

    btn.click();

    return 0;
    }

    当然,上面的代码照Qt的信号槽差得还很远,但这就入门了。下来只需要按照自己的需求不断的完善即可。掌握了建造方式,那么剩下的就只是不断的在功能、安全和接口上不断的下功夫了。

    五、总结

    常说的站在巨人的肩膀上,不是简单的跳上去。否则的话,巨人不高兴还会赶人下来的。跳上去后,看到更远处的风景,就要学会自己变成巨人。如此迭代,人类社会就是么进步。同样,技术也是如此。

    赞(0)
    未经允许不得转载:171主机测评 » C++编程实践—C++实现类似Qt的信号槽机制
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址