sigslot: 一个轻量级实现观察者模式的C++开源库

目录

1.简介

2.安装方式

3.使用示例

4.线程安全

4.1.线程安全保护

4.2.对象拷贝问题

[5.与 Qt/Boost 信号机制的关键差异](#5.与 Qt/Boost 信号机制的关键差异)

6.总结


1.简介

sigslot 库是 C++ 中轻量级实现观察者模式的开源库,核心用于解耦事件发送者与接收者,无依赖、易集成,相比 Qt 信号槽无需元对象系统(moc),相比 Boost.Signals2 更轻量化,广泛用于非 Qt 项目的事件驱动开发。

核心底层原理:

  1. 核心机制 :基于回调函数封装,信号(signal)本质是可动态增减的回调列表,槽(slot)是回调函数(支持全局函数、成员函数、lambda 等)。
  2. 类型安全 :编译期检查信号与槽的参数类型匹配,避免运行时错误,部分实现借助std::function和模板实现类型推导。
  3. 线程安全:主流版本(如 sigslot 2.0+)支持可选线程安全模式,通过互斥锁保护回调列表的增删操作,支持跨线程信号发射(需手动配置队列模式)。
  4. 无虚函数开销:相比 Qt 信号槽的虚函数调用,sigslot 多数实现无额外虚函数开销,内存占用更紧凑。

2.安装方式

1.从sigslot GitHub 仓库下载源码或下面地址下载(推荐 2.0 + 版本,支持 C++11+)。

http://sigslot.sourceforge.net/

说明文档:

http://sigslot.sourceforge.net/sigslot.pdf

2.解压后,将include目录下的sigslot文件夹复制到项目的第三方库头文件目录(如third_party/include)。

3.无需编译静态库 / 动态库,直接在项目中包含头文件即可使用(纯头文件库特性)。

3.使用示例

1.基础信号 - 槽连接(成员函数 + 全局函数)

cpp 复制代码
#include <sigslot/sigslot.h>
#include <iostream>
#include <string>

// 全局槽函数
void global_slot(const std::string& msg) {
    std::cout << "Global slot: " << msg << std::endl;
}

class Sender {
public:
    sigslot::signal<const std::string&> sig_send; // 单参数信号
};

class Receiver {
public:
    void member_slot(const std::string& msg) { // 成员槽函数
        std::cout << "Member slot: " << msg << std::endl;
    }
};

int main() {
    Sender sender;
    Receiver receiver;

    // 连接成员函数槽
    sender.sig_send.connect(&receiver, &Receiver::member_slot);
    // 连接全局函数槽
    sender.sig_send.connect(global_slot);

    // 发射信号
    sender.sig_send.emit("Basic sigslot test");

    // 断开指定连接
    sender.sig_send.disconnect(&receiver, &Receiver::member_slot);
    return 0;
}

2.进阶场景:lambda 槽 + 多参数信号

cpp 复制代码
#include <sigslot/sigslot.h>
#include <iostream>

class Calculator {
public:
    // 多参数信号:int(操作数1), int(操作数2), std::string(操作符)
    sigslot::signal<int, int, const std::string&> sig_calculate;
};

int main() {
    Calculator calc;

    // 连接lambda表达式作为槽(自动匹配参数)
    calc.sig_calculate.connect([](int a, int b, const std::string& op) {
        if (op == "+") std::cout << "Result: " << a + b << std::endl;
        else if (op == "*") std::cout << "Result: " << a * b << std::endl;
    });

    // 发射多参数信号
    calc.sig_calculate.emit(10, 20, "+");
    calc.sig_calculate.emit(10, 20, "*");
    return 0;
}

3.线程安全模式启用

sigslot 2.0 + 默认线程不安全,需通过宏定义启用线程安全,支持跨线程信号发射:

cpp 复制代码
#define SIGSLOT_DEFAULT_MT_POLICY sigslot::multi_threaded // 定义在包含头文件前
#include <sigslot/sigslot.h>
#include <thread>
#include <iostream>

class ThreadSender {
public:
    sigslot::signal<std::string> sig_thread_msg;
};

int main() {
    ThreadSender sender;

    // 主线程连接槽
    sender.sig_thread_msg.connect([](const std::string& msg) {
        std::cout << "Thread slot: " << msg << std::endl;
    });

    // 子线程发射信号
    std::thread t([&sender]() {
        sender.sig_thread_msg.emit("Message from sub-thread");
    });
    t.join();

    return 0;
}

4.线程安全

4.1.线程安全保护

1)single_threaded:单线程模型,不需要考虑并发等情况,也就不需要线程保护机制了。所有的信号槽的操作都在一个线程中调用。

2)multi_threaded_global:全局多线程模型,适用于多线程并发的情况,所有信号槽的操作都将由一个全局的线程锁保护,资源占用更少,但也容易出现某个信号会因为其他信号产生资源竞争而形成阻塞等待的情况(时间会较长)。

3)multi_threaded_local:本地多线程模型,同样适用于多线程并发的情况,每个信号槽都拥有各自的线程所保护,好处是只有该信号自身出现资源竞争才会形成阻塞等待的情况(时间相对全局来说短很多),但也同时占用系统资源更多,可能会影响操作系统的运行速度。

线程模型的使用方法:

1)隐式指定

cpp 复制代码
// 这里定义了该宏,并且指定为 single_threaded 模型
#define SIGSLOT_DEFAULT_MT_POLICY single_threaded
 
// 如果没有定义默认线程保护方式
#ifndef SIGSLOT_DEFAULT_MT_POLICY
#	ifdef _SIGSLOT_SINGLE_THREADED	// 如果定义了不使用任何锁保护
#		define SIGSLOT_DEFAULT_MT_POLICY single_threaded  // 则默认为不使用任何锁保护
#	else
#		define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_local // 否则默认以 multi_threaded_local 方式进行保护
#	endif
#endif
cpp 复制代码
// 在信号定义,这里默认指定了 SIGSLOT_DEFAULT_MT_POLICY
template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> 
class signal0 : public _signal_base0<mt_policy>
{
public:
	typedef _signal_base0<mt_policy> base;
	...
	...
	...
};
cpp 复制代码
// 在槽定义,这里默认指定了 SIGSLOT_DEFAULT_MT_POLICY
template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
class has_slots : public has_slots_interface, public mt_policy
{
private:
	typedef std::set<_signal_base_interface*> sender_set;
	typedef sender_set::const_iterator const_iterator;
 
public:
		...
		...
	has_slots(const has_slots& hs)
	{
		lock_block<mt_policy> lock(this);
		
		...
		...
	}
};

在定义信号与槽时,不指定线程模型,可以通过定义宏 SIGSLOT_DEFAULT_MT_POLICY来默认指定某一个线程模型。

2)显式指定

通过上面也可知道,可以通过定义信号以及继承槽时,指定线程模型。

cpp 复制代码
// 定义三个信号
signal1<string, multi_threaded_local>	        sigConnecting;			// 正在连接的信号
signal0<multi_threaded_local>			sigConnected;			// 连接成功的信号
signal0<multi_threaded_local>			sigDisConnect;			// 断开连接的信号
cpp 复制代码
// 负责播报提示音的类(槽)
class Tips : public has_slots<multi_threaded_local>
{
 
public:
 
	...
 
};
 
// 负责显示的类(如UI、动画等等)(槽)
class Display : public has_slots<multi_threaded_local>
{
 
public:
 
	...
 
};

4.2.对象拷贝问题

在文档中,作者有提到对象拷贝的实现方式,但通过阅读源码,发现并没有重载赋值运算符的实现,因此特地写了一个代码进行试验,发现确实会出现问题。

cpp 复制代码
int main()
{
	signal0<multi_threaded_local>			sigConnected1;
	signal0<multi_threaded_local>			sigConnected2;
 
	Display			mDisplay;
	Tips			mTips;
 
	sigConnected1.connect(&mTips, &Tips::onPlayNetworkConnected);
	sigConnected1.connect(&mDisplay, &Display::onShowNetworkConnected);
 
	printf("sigConnected1.emit() start...\n");
	sigConnected1.emit();
	printf("sigConnected1.emit() end...\n");
	puts("-----------------------------------------------------------");
 
	printf("sigConnected2 = sigConnected1\n");
	sigConnected2 = sigConnected1;
	printf("sigConnected2.emit() start...\n");
	sigConnected2.emit();
	printf("sigConnected2.emit() end...\n");
	puts("-----------------------------------------------------------");
 
	printf("sigConnected1.disconnect(&mTips)\n");
	sigConnected1.disconnect(&mTips);
 
	printf("sigConnected1.emit() start...\n");
	sigConnected1.emit();
	printf("sigConnected1.emit() end...\n");
	puts("-----------------------------------------------------------");
 
	printf("sigConnected2.emit() start...\n");
	sigConnected2.emit();						// 这里会出现异常, 如果是在 Linux 则会报段错误
	printf("sigConnected2.emit() end...\n");
	puts("-----------------------------------------------------------");
 
	return 0;
}

通过实际测试可以发现,在信号1 sigConnected1.disconnect(&mTips); 之后,信号1 是正常的,但是信号2在触发时,就出现了问题。

通过分析代码,当我们调用 connect 进行信号关联时会 new 一个新的对象加入到 list 中,而调用 disconnect 解除关联时,会 delete 该对象。

cpp 复制代码
template<class desttype>
void connect(desttype* pclass, void (desttype::* pmemfun)())
{
	lock_block<mt_policy> lock(this);
	_connection0<desttype, mt_policy>* conn =
		new _connection0<desttype, mt_policy>(pclass, pmemfun);	// new 了一个对象
	m_connected_slots.push_back(conn);	// 将对象地址存入
	pclass->signal_connect(this);
}
 
 
void disconnect(has_slots_interface* pclass)
{
	lock_block<mt_policy> lock(this);
	typename connections_list::iterator it = m_connected_slots.begin();
	typename connections_list::iterator itEnd = m_connected_slots.end();
 
	while (it != itEnd)
	{
		if ((*it)->getdest() == pclass)
		{
			delete* it;						// 释放内存
			m_connected_slots.erase(it);	// 从 list 中移除该项
			pclass->signal_disconnect(this);
			return;
		}
 
		++it;
	}
}

由于没有重载赋值运算符,编译器将会自动生成赋值运算符相关的代码,只做到了浅拷贝,因此我们把信号1赋值给信号2,只是拷贝了 list 里面的数据,即 sigConnected1.connect() 时 new 的地址。当调用 sigConnected1.disconnect() 时,就会 delete 该对象,但是 sigConnected2 里面的 list 却仍保留着该对象地址,所以在调用 sigConnected2.emit(); 就会去访问已经被释放了的对象,从而产生错误。

解决方法就是修改 sigslot 库,为每一个信号实现深拷贝,如不带参数的 signal0:

cpp 复制代码
// add lmx: https://me.csdn.net/lovemengx  -- 2020-04-12
signal0 &operator=(const signal0 &singnal)
{
	if (this != &singnal) 
	{
		lock_block<mt_policy> lock(this);
		typename connections_list::const_iterator itNext, it = singnal.m_connected_slots.begin();
		typename connections_list::const_iterator itEnd = singnal.m_connected_slots.end();
 
		while (it != itEnd)
		{
			this->m_connected_slots.push_back((*it)->clone());
			(*it)->getdest()->signal_connect(this);
 
			itNext = it;
			++itNext;
			it = itNext;
		}
	}
	return *this;
}

5.与 Qt/Boost 信号机制的关键差异

特性 sigslot 库 Qt 信号槽 Boost.Signals2
依赖 无(仅依赖 C++11+) 依赖 Qt 元对象系统 依赖 Boost 核心库
编译依赖 无需额外编译步骤 需 moc 预处理 需 Boost 编译环境
核心优势 极致轻量化、无冗余开销 集成 Qt 生态、支持跨线程 功能全面(组播、自动断开)
适用场景 非 Qt 轻量项目、性能敏感场景 Qt UI / 跨线程项目 大型 Boost 生态项目

6.总结

  • 该开源库支持最多 8 个参数的信号定义,不够可以自行扩展,不过一般一个参数就够了,传一个结构体或者对象地址
  • 信号与槽函数的对应,只和参数类型、参数个数有关,如果一致,那么就可以进行关联。
  • 槽函数的返回值需是 void 类型,且槽函数所属的类必须继承 has_slots<>,如果需要判断返回值可以自行实现。
  • 槽函数的调用顺序与连接信号顺序一致,因为采用的是 list 实现,所以没有优先级之分,不过可以自己修改实现。
  • 该开源库属于同步通知类型,即一个信号对应多个槽函数,只要有一个槽函数阻塞,那么后面的槽函数就无法被调用,信号调用的.emit() 也会阻塞。这点,可以通过查看 emit 的实现即可确认。
相关推荐
千千道1 小时前
QT上位机作为FTP客户端上传多文件
c++·qt
屿筱1 小时前
vscode 关于C/C++的环境配置
c++·ide·vscode
程序喵大人1 小时前
C++ MCP 服务器实现
开发语言·c++·项目·mcp服务器
小尧嵌入式1 小时前
QT软件开发知识点流程及文本转语音工具
开发语言·c++·qt
500841 小时前
鸿蒙 Flutter 插件二次开发:基于开源插件(如 flutter_downloader)适配鸿蒙【实战指南】
flutter·华为·electron·开源·音视频·开源鸿蒙
MessiGo1 小时前
Bézier 曲线
c++
艾莉丝努力练剑1 小时前
【C++:智能指针】没有垃圾回收?智能指针来也!破解C++内存泄漏:智能指针原理、循环引用与线程安全详解
大数据·运维·c++·安全·编辑器·智能指针
修己xj1 小时前
告别数字麻木,重拾消费感知:ezBookkeeping —— 您的轻量自托管记账伴侣
开源
橘子真甜~2 小时前
C/C++ Linux网络编程9 - TCP服务器实现流程和独立运行
linux·运维·服务器·c++·守护进程·会话组