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

一、说明

学习一个技术的最经典的方式是什么?就是重复造轮子。大到国家小到个人,基本都是这样,先引进消化,吸收后才能够慢慢进行创新。所谓引进消化,其实就是自己造轮子,只有造好轮子,真正掌握其中的技术原理和细节,才能够"知其所以然"。

在前面分析了很多有名气的开源软件框架的源码,其实也是这个意思。从底层掌握设计、开发的全流程,然后才能开始用其优秀之处进行自己的实际应用。在实际中融会贯通的理解优秀的底层实现逻辑后,才能够据此进行更多更好的创新。

二、Qt的信号-槽机制

Qt作为一个在C++中的优秀平台,广泛应用于GUI和跨平台的开发。在Qt中,其基础的信号和槽机制是GUI开发的重要一环。信号-槽机制本质是一种实现了对类型安全、对象生命周期管理、跨线程派发的观察者机制,其表现形式仍然是函数。在Qt中,为了实现的方便,提供了MOC的元编程机制(Q_OBJECT);为了匹配的适应性,也提供了多种的连接类型。Qt提供了对信号槽机制的安全保证即生命周期的安全控制管理。在跨线程应用时,只要小心使用,也是没有大问题的。

需要注意的是,在新的支持Lambda表达式的机制中,要小心生命周期的不匹配。信号机制决定了它在跨线程高频使用时可能会遇到一些问题,如队列的堆积、处理的效率等。特别是大数据的传递,最好采用指针或共享数据(可能需要锁)。

那么Qt中的信号和槽到底是什么呢?信号和槽,Signals and Slots。是一种高级的回调机制,它提供了对象之间的通信方式并提高了代码的可读性和维护性。信号和槽其实誻一个发送者对象(Sender)通过发射(Emitting)一个信号(Signal)来通知另一个接收者对象(Receiver)某个事件的发生。它们之间通过槽(Slot)来实现对事件的响应。

其基本的流程如下:

  1. 声明信号

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

    c 复制代码
    signals:
     void dataChanged(int d);
  2. 声明槽

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

    c++ 复制代码
    public slots:
     void onDataChanged(int d);
  3. 建立连接

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

    c 复制代码
    connect(sender, &Sender::dataChanged,
         receiver, &Receiver::onDataChanged);
  4. 发送信号

    真正的触发事件,其方式基本为:

    c 复制代码
    emit valueChanged(1);

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

三、C++的实现

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

  1. 创建函数以及函数的传递需要函数对象std::function
  2. 信号和槽函数的处理需要std::bind
  3. 参数转发处理可能需要std::forward
  4. 参数及对象控制需要智能指针
  5. 如果再复杂一些需要支持Lambda表达式
  6. 模板技术特别是变参模板

这可以算是一个相对简单实现的流程,它不需要MOC和反射,也不需要支持连接类型等,只从原理上实现相关的信号槽机制。明白了相关实现需要的技术,下面看一个最基础的实现:

c 复制代码
#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信号槽机制的入门方式,即造一个长得有点像的最简单轮子。下面再看一个相对复杂的实现:

c 复制代码
#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的信号槽差得还很远,但这就入门了。下来只需要按照自己的需求不断的完善即可。掌握了建造方式,那么剩下的就只是不断的在功能、安全和接口上不断的下功夫了。

五、总结

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

相关推荐
格发许可优化管理系统2 小时前
Mentor许可证使用规定全解析
java·大数据·c语言·开发语言·c++
郝学胜_神的一滴2 小时前
Qt 高级开发 030:QListWidget 右键菜单全解,从策略配置到精准删除的优雅实现
c++·qt
海天鹰2 小时前
图片去黑边算法
qt·算法
攻城狮Soar2 小时前
STL源码解析之list(1)
开发语言·c++
2401_869769592 小时前
内容5 日期类实现
开发语言·c++
xxwl5852 小时前
一个原创题(二)
c++·算法
MZZ骏马3 小时前
C++ 极简模式的日志
c++
AbandonForce3 小时前
滑动窗口:定长滑动窗口与不定长滑动窗口
数据结构·c++·算法
小欣加油4 小时前
leetcode3689最大子数组总值I
c++·算法·leetcode·职场和发展·贪心算法