C++实现一个简单的Qt信号槽机制(1)

昨天写这个文章《深入探讨C++的高级反射机制(2):写个能用的反射库》的时候就在想,是不是也能在这套反射逻辑的基础上,实现一个类似Qt的信号槽机制?

Qt信号槽机制简介

所谓的Qt的信号槽(Signals and Slots)机制,是Qt框架中实现对象之间通信的一种方式。这是一个事件驱动程序中常见的设计模式。信号槽机制允许创建响应特定事件(如用户交互、数据变化等)的可重用组件。
信号槽主要有以下核心概念组成:

信号(Signals)

信号是一个类成员函数的声明,它在类内部以 signals: 关键词标识。当某个事件发生时,可以发射(emit)信号。信号不包含具体的实现代码,只是一个通知机制。它告诉外界某个事件已经发生,比如按钮被点击或者定时器超时。

槽(Slots)

槽是一个普通的成员函数,可以是公有的、保护的或私有的,它在类内部以 slots: 关键词标识(Qt 5 开始,普通的成员函数也可以作为槽)。槽函数包含了当信号发射时应该执行的代码。换句话说,槽函数是对信号的响应。

连接(Connection)

信号和槽之间的连接是通过 QObject::connect() 函数建立的。这个连接指定了当信号发射时,应该调用哪个槽函数。一个信号可以连接到多个槽,一个槽也可以接收来自多个信号的通知。

示例

以下是一个简单的Qt信号和槽的例子,展示了这个机制如何工作:

#include <QObject>

class Button : public QObject {
   
    Q_OBJECT

public:
    Button() {
   }

signals:
    void clicked(); // 信号声明

public slots:
    void onClick() {
    // 槽声明
        // 处理按钮点击事件
    }
};

int main() {
   
    Button button;
    // 连接按钮的 clicked 信号到同一个按钮的 onClick 槽
    QObject::connect(&button, &Button::clicked, &button, &Button::onClick);

    // 在某个地方,按钮被点击,发射信号
    emit button.clicked();

    return 0;
}

#include "main.moc" // 如果使用qmake或CMake,通常不需要这一行

在这个例子中,当按钮被点击时,它会发射 clicked 信号,这会导致调用与它连接的 onClick 槽函数。

信号槽机制的优点在于它提供了一种松耦合的方式来处理事件。对象不需要知道哪些对象或函数对它们的信号感兴趣,它们只需在合适的时候发射信号。这样可以创建可重用和可维护的组件,同时简化了应用程序的事件处理逻辑。

我们的实现思路

为了实现类似于Qt信号槽的机制,我们需要一个类似QObject的基类。为了避免引入新概念,我们这个类也直接较QObject好了。类中实现信号的发射(emit)和槽的连接(connect)。
笔者不太喜欢Qt的connect函数是个静态函数,所以我们这里的实现稍微和Qt不一样,我们的connect函数是个普通成员函数,用于将自己的信号连接到目标槽上。
接下来,我们需要声明信号的机制。我们通过定义宏DECL_SIGNAL来声明一个信号,并实现相应的连接和断开连接的逻辑。
于是,我们的信号槽大概用法如下:


// 用户自定义的结构体
class MyStruct : public refl::QObject // 信号槽等功能从这个类派生
{
   
public:
	// 定义一个方法,用作槽函数,必须在REFLECTABLE_MENBER_FUNCS列表中,并且参数必须是std::any,不能超过4个参数。
	std::any on_x_value_modified(std::any new_value) {
   
		int value = std::any_cast<int>(new_value);
		std::cout << "MyStruct::on_x_value_modified called! New value is: " << value << std::endl;
		return 0;
	}
	REFLECTABLE_MENBER_FUNCS(MyStruct,
		REFLEC_FUNCTION(on_x_value_modified)
	);
	DECL_SIGNAL(x_value_modified, int) // 声明信号x_value_modified
	DECL_DYNAMIC_REFLECTABLE(MyStruct)//动态反射的支持
};

// 信号槽的连接和调用:

MyStruct obj1;
MyStruct obj2;

// 连接obj1的信号到obj2的槽函数
size_t connection_id = obj1.**connect**("x_value_modified", &obj2, "on_x_value_modified");
if (connection_id != 0) {
   
	std::cout << "Signal x_value_modified from obj1 connected to on_x_value_modified slot in obj2." << std::endl;
}
obj1.x_value_modified(42);// 触发信号

// 断开连接
obj1.**disconnect**(connection_id);
// 再次触发信号,应该没有任何输出,因为已经断开连接
obj1.x_value_modified(84);

有了用法的情况下,我们就有了目标了。
这个是我们DECL_SIGNAL和QObject的实现:


	//宏用于类中声明信号,并提供一个同名的方法来触发信号。
	#define DECL_SIGNAL(signal_name, ...) \
		template<typename... Args> \
		void signal_name(Args&&... args) {
      \
			emit_signal_impl(#signal_name, std::forward<Args>(args)...); \
		} \

	class QObject : public refl::dynamic::IReflectable {
   
	private:
		// 信号与槽的映射,键是信号名称,值是一组槽函数的信息
		std::unordered_map<std::string, std::vector<std::pair<QObject*, std::string>>> connections;
		size_t next_connection_id = 1;
		std::map<size_t, std::pair<std::string, std::pair<QObject*, std::string>>> connection_map;

	public:
		template<typename... Args>
		void emit_signal_impl(const char* signal_name, Args&&... args) {
   
			auto it = connections.find(signal_name);
			if (it != connections.end()) {
   
				for (auto& slot_info : it->second) {
   
					slot_info.first->invoke_member_func_by_name(slot_info.second.c_str(), std::forward<Args>(args)...);
					//invoke_member_func_type_safe(*slot_info.first, slot_info.second.c_str(), std::forward<Args>(args)...); 
				}
			}
		}

		size_t connect(const char* signal_name, QObject* target, const char* target_member_func_name) {
   
			if (!target || !signal_name || !target_member_func_name) return 0;
			connections[signal_name].emplace_back(target, target_member_func_name);
			size_t id = next_connection_id++;
			connection_map[id] = {
    signal_name, {
   target, target_member_func_name} };
			return id;
		}

		bool disconnect(size_t connection_id) {
   
			auto it = connection_map.find(connection_id);
			if (it != connection_map.end()) {
   
				auto& [signal_name, slot_info] = it->second;
				auto& slots = connections[signal_name];
				slots.erase(std::remove(slots.begin(), slots.end(), slot_info), slots.end());
				connection_map.erase(it);
				return true;
			}
			return false;
		}
	};

运行起来,还不错:
在这里插入图片描述

但是这段代码很不优雅:

size_t connection_id = obj1.connect("x_value_modified", &obj2, "on_x_value_modified");

因为都是字符串,万一打错了单词还不容易发现。我们是否可以优化成这种形式:

size_t connection_id = obj1.connect(&MyStruct::x_value_modified, &obj2, &MyStruct::on_x_value_modified);

实现这种形式也不难,我们需要对connect方法进行重载,使其能接受成员函数指针而不是字符串。并能从成员函数指针中提取其函数名称。

template <typename SignalClass, typename SignalType, typename SlotClass, typename SlotType>
	size_t connect(SignalType SignalClass::*signal, SlotClass* slot_instance, SlotType SlotClass::*slot) {
   
		const char* signal_name = get_member_func_name<SignalClass>(signal);
		const char* slot_name = get_member_func_name<SlotClass>(slot);
		if (signal_name && slot_name) {
   
			return connect(signal_name, static_cast<QObject*>(slot_instance), slot_name);
		}
		return 0; // Failed
	}

由于我们已经有了之前反射库的实现经验,get_member_func_name的实现也信手拈来:

template <typename T, typename FuncTuple, size_t N = 0>
constexpr const char* __get_member_func_name_impl(void* func_ptr, const FuncTuple& tp) {
   
	if constexpr (N >= std::tuple_size_v<FuncTuple>) {
   
		return nullptr; // Not Found!
	} else {
   
		const auto& func = std::get<N>(tp);
		if (reinterpret_cast<void*>(func.get_func()) == func_ptr) {
   
			return func.name;
		} else {
   
			return __get_member_func_name_impl<T, FuncTuple, N + 1>(func_ptr
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值