Sigslot 是 C++ 信号槽的纯头文件、线程安全实现。
主要目标是取代 Boost.Signals2。
除了通常的功能外,它还提供
Sigslot 经过单元测试,应该足够可靠和稳定,可以取代 Boost Signals2。
测试在地址、线程和未定义行为清理器下干净地运行。
许多实现允许信号返回类型,但 Sigslot 不允许,因为我对它们没有用处。如果我能确信不这样的话,我以后可能会改变主意。
无需编译或安装,只需包含sigslot/signal.hpp
并使用即可。 Sigslot 目前依赖于 C++14 兼容编译器,但如果需要,它可能会改装为 C++11。众所周知,它可以在 GNU Linux、MSVC 2017 及更高版本上与 Clang 4.0 和 GCC 5.0+ 编译器配合使用,在 Windows 上与 Clang-cl 和 MinGW 配合使用。
但是,请注意使用 MSVC 和 Clang-Cl 编译器的 Windows 上的潜在问题,在特殊情况下可能需要/OPT:NOICF
链接器标志。请阅读“实施细节”一章以获取解释。
提供 CMake 列表文件用于安装目的并生成 CMake 导入模块。这是首选安装方法。 Pal::Sigslot
导入的目标可用,并且已经应用了所需的链接器标志。示例和测试也需要它,可以选择依赖 Qt5 和 Boost 来进行适配器单元测试。
# Using Sigslot from cmake
find_package (PalSigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
配置选项SIGSLOT_REDUCE_COMPILE_TIME
在配置时可用。激活后,它会尝试通过避免调用std::make_shared
导致的大量模板实例化来减少代码膨胀。默认情况下此选项处于关闭状态,但对于那些希望在效率稍低的代码范围内偏爱代码大小和编译时间的人可以激活此选项。
可以使用根目录中的以下说明完成安装:
mkdir build && cd build
cmake .. -DSIGSLOT_REDUCE_COMPILE_TIME=ON -DCMAKE_INSTALL_PREFIX= ~ /local
cmake --build . --target install
# If you want to compile examples:
cmake --build . --target sigslot-examples
# And compile/execute unit tests:
cmake --build . --target sigslot-tests
Pal::Sigslot
也可以使用 FetchContent 方法集成。
include (FetchContent)
FetchContent_Declare(
sigslot
GIT_REPOSITORY https://github.com/palacaze/sigslot
GIT_TAG 19a6f0f5ea11fc121fe67f81fd5e491f2d7a4637 # v1.2.0
)
FetchContent_MakeAvailable(sigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
Sigslot 实现了 UI 框架中流行的信号槽结构,使得观察者模式或基于事件的编程变得容易使用。该库的主要入口点是sigslot::signal<T...>
类模板。
信号是一个可以发出类型化通知的对象,实际上是在信号类模板参数之后参数化的值,并注册任意数量的兼容参数类型的通知处理程序(可调用对象),以便在信号发射发生时使用提供的值执行。在信号槽术语中,这称为将槽连接到信号,其中“槽”表示可调用实例,而“连接”可以被认为是从信号到槽的概念链接。
下面提供的所有代码片段都可以在示例子目录中以可编译源代码的形式提供。
这是第一个示例,展示了该库最基本的功能。
我们首先声明一个无参数信号sig
,然后我们继续连接几个插槽,最后发出一个信号,该信号触发调用预先连接的每个可调用插槽。请注意该库如何处理不同形式的可调用对象。
# include < sigslot/signal.hpp >
# include < iostream >
void f () { std::cout << " free function n " ; }
struct s {
void m () { std::cout << " member function n " ; }
static void sm () { std::cout << " static member function n " ; }
};
struct o {
void operator ()() { std::cout << " function object n " ; }
};
int main () {
s d;
auto lambda = []() { std::cout << " lambda n " ; };
auto gen_lambda = []( auto && ... a ) { std::cout << " generic lambda n " ; };
// declare a signal instance with no arguments
sigslot:: signal <> sig;
// connect slots
sig. connect (f);
sig. connect (&s::m, &d);
sig. connect (&s::sm);
sig. connect ( o ());
sig. connect (lambda);
sig. connect (gen_lambda);
// a free connect() function is also available
sigslot::connect (sig, f);
// emit a signal
sig ();
}
默认情况下,发出信号时的槽调用顺序是未指定的,请不要依赖它总是相同的。您可以使用槽组来限制特定的调用顺序,稍后将介绍这些顺序。
第一个示例很简单,但不太有用,让我们继续讨论发出值的信号。信号可以发出任意数量的参数,如下所示。
# include < sigslot/signal.hpp >
# include < iostream >
# include < string >
struct foo {
// Notice how we accept a double as first argument here.
// This is fine because float is convertible to double.
// 's' is a reference and can thus be modified.
void bar ( double d, int i, bool b, std::string &s) {
s = b ? std::to_string (i) : std::to_string (d);
}
};
// Function objects can cope with default arguments and overloading.
// It does not work with static and member functions.
struct obj {
void operator ()( float , int , bool , std::string &, int = 0 ) {
std::cout << " I was here n " ;
}
void operator ()() {}
};
int main () {
// declare a signal with float, int, bool and string& arguments
sigslot:: signal < float , int , bool , std::string&> sig;
// a generic lambda that prints its arguments to stdout
auto printer = [] ( auto a, auto && ... args ) {
std::cout << a;
( void )std::initializer_list< int >{
(( void )(std::cout << " " << args), 1 )...
};
std::cout << " n " ;
};
// connect the slots
foo ff;
sig. connect (printer);
sig. connect (&foo::bar, &ff);
sig. connect ( obj ());
float f = 1 . f ;
short i = 2 ; // convertible to int
std::string s = " 0 " ;
// emit a signal
sig (f, i, false , s);
sig (f, i, true , s);
}
如图所示,槽参数类型不需要与信号模板参数严格相同,可以进行转换就可以了。通用参数也很好,如printer
通用 lambda 所示(也可以编写为函数模板)。
现在,我可以想到关于可调用处理的两个限制:默认参数和函数重载。对于函数对象,两者都可以正常工作,但由于不同但相关的原因,无法使用静态函数和成员函数进行编译。
考虑下面的代码:
struct foo {
void bar ( double d);
void bar ();
};
&foo::bar
应该指什么?根据重载,成员函数上的指针不会映射到唯一的符号,因此编译器将无法选择正确的符号。解析正确符号的一种方法是将函数指针显式转换为正确的函数类型。这是一个示例,它使用一个小帮助工具来实现更轻的语法(事实上,我可能很快就会将其添加到库中)。
# include < sigslot/signal.hpp >
template < typename ... Args, typename C>
constexpr auto overload ( void (C::*ptr)(Args...)) {
return ptr;
}
template < typename ... Args>
constexpr auto overload ( void (*ptr)(Args...)) {
return ptr;
}
struct obj {
void operator ()( int ) const {}
void operator ()() {}
};
struct foo {
void bar ( int ) {}
void bar () {}
static void baz ( int ) {}
static void baz () {}
};
void moo ( int ) {}
void moo () {}
int main () {
sigslot:: signal < int > sig;
// connect the slots, casting to the right overload if necessary
foo ff;
sig. connect (overload< int >(&foo::bar), &ff);
sig. connect (overload< int >(&foo::baz));
sig. connect (overload< int >(&moo));
sig. connect ( obj ());
sig ( 0 );
return 0 ;
}
默认参数不是函数类型签名的一部分,并且可以重新定义,因此它们确实很难处理。将插槽连接到信号时,库确定是否可以使用信号参数类型调用所提供的可调用函数,但此时默认函数参数的存在未知,因此参数数量可能不匹配。
针对此用例的一个简单解决方法是创建一个绑定适配器,事实上我们甚至可以使其非常通用,如下所示:
# include < sigslot/signal.hpp >
# define ADAPT ( func )
[=]( auto && ...a) { (func)(std::forward< decltype (a)>(a)...); }
void foo ( int &i, int b = 1 ) {
i += b;
}
int main () {
int i = 0 ;
// fine, all the arguments are handled
sigslot:: signal < int &, int > sig1;
sig1. connect (foo);
sig1 (i, 2 );
// must wrap in an adapter
i = 0 ;
sigslot:: signal < int &> sig2;
sig2. connect ( ADAPT (foo));
sig2 (i);
return 0 ;
}
直到现在还不清楚的是signal::connect()
实际上返回一个sigslot::connection
对象,该对象可用于管理信号槽连接的行为和生命周期。 sigslot::connection
是一个轻量级对象(基本上是std::weak_ptr
),它允许与正在进行的信号槽连接交互并公开以下功能:
signal::connect()
创建的连接。 sigslot::connection
不会将连接绑定到作用域:这不是 RAII 对象,这解释了为什么它可以被复制。然而,它可以隐式转换为sigslot::scoped_connection
,当超出范围时,它会破坏连接。
以下是说明其中一些功能的示例:
# include < sigslot/signal.hpp >
# include < string >
int i = 0 ;
void f () { i += 1 ; }
int main () {
sigslot:: signal <> sig;
// keep a sigslot::connection object
auto c1 = sig. connect (f);
// disconnection
sig (); // i == 1
c1. disconnect ();
sig (); // i == 1
// scope based disconnection
{
sigslot::scoped_connection sc = sig. connect (f);
sig (); // i == 2
}
sig (); // i == 2;
// connection blocking
auto c2 = sig. connect (f);
sig (); // i == 3
c2. block ();
sig (); // i == 3
c2. unblock ();
sig (); // i == 4
}
Sigslot 支持扩展插槽签名,并附加sigslot::connection
引用作为第一个参数,这允许从插槽内部进行连接管理。可以使用connect_extended()
方法访问此扩展签名。
# include < sigslot/signal.hpp >
int main () {
int i = 0 ;
sigslot:: signal <> sig;
// extended connection
auto f = []( auto &con) {
i += 1 ; // do work
con. disconnect (); // then disconnects
};
sig. connect_extended (f);
sig (); // i == 1
sig (); // i == 1 because f was disconnected
}
用户必须确保插槽的寿命超过信号的寿命,这在复杂的软件中可能会变得乏味。为了简化此任务,Sigslot 可以自动断开它能够跟踪其生命周期的槽对象。为此,槽必须可转换为某种形式的弱指针。
std::shared_ptr
和std::weak_ptr
是开箱即用的,并且提供适配器来支持boost::shared_ptr
、 boost::weak_ptr
和 Qt QSharedPointer
、 QWeakPointer
以及从QObject
派生的任何类。
可以通过声明to_weak()
适配器函数来添加其他可跟踪对象。
# include < sigslot/signal.hpp >
# include < sigslot/adapter/qt.hpp >
int sum = 0 ;
struct s {
void f ( int i) { sum += i; }
};
class MyObject : public QObject {
Q_OBJECT
public:
void add ( int i) const { sum += i; }
};
int main () {
sum = 0 ;
signal < int > sig;
// track lifetime of object and also connect to a member function
auto p = std::make_shared<s>();
sig. connect (&s::f, p);
sig ( 1 ); // sum == 1
p. reset ();
sig ( 1 ); // sum == 1
// track an unrelated object lifetime
struct dummy ;
auto l = [&]( int i) { sum += i; };
auto d = std::make_shared<dummy>();
sig. connect (l, d);
sig ( 1 ); // sum == 2
d. reset ();
sig ( 1 ); // sum == 2
// track a QObject
{
MyObject o;
sig. connect (&MyObject::add, &o);
sig ( 1 ); // sum == 3
}
sig ( 1 ); // sum == 3
}
确保成员函数槽上的指针自动断开连接的另一种方法是显式继承sigslot::observer
或sigslot::observer_st
。前者是线程安全的,与后者相反。
这是一个用法示例。
# include < sigslot/signal.hpp >
int sum = 0 ;
struct s : sigslot::observer_st {
void f ( int i) { sum += i; }
};
struct s_mt : sigslot::observer {
~s_mt () {
// Needed to ensure proper disconnection prior to object destruction
// in multithreaded contexts.
this -> disconnect_all ();
}
void f ( int i) { sum += i; }
};
int main () {
sum = 0 ;
signal < int > sig;
{
// Lifetime of object instance p is tracked
s p;
s_mt pm;
sig. connect (&s::f, &p);
sig. connect (&s_mt::f, &pm);
sig ( 1 ); // sum == 2
}
// The slots got disconnected at instance destruction
sig ( 1 ); // sum == 2
}
使用这种侵入方法的对象可以连接到任意数量的不相关信号。
1.2.0 版本中引入了通过提供适当的函数签名、对象指针或跟踪器来支持插槽断开连接。
可以使用signal::disconnect()
方法断开任意数量的槽,该方法提出 4 个重载来指定断开标准:
由于 lambda 的唯一性,只有绑定到变量的 lambda 才可能断开 lambda。
第二个重载当前需要 RTTI 来断开与成员函数、函数对象和 lambda 的指针的连接。此限制不适用于自由和静态成员函数。原因源于这样一个事实:在 C++ 中,指向不相关类型的成员函数的指针不具有可比性,这与指向 free 和 static 成员函数的指针相反。例如,指向不同类的虚方法的成员函数的指针可以具有相同的地址(它们将方法的偏移量存储到虚函数表中)。
然而,Sigslot 可以在禁用 RTTI 的情况下进行编译,并且对于有问题的情况将停用重载。
作为一个辅助节点,这个功能无可否认地添加了比最初预期更多的代码,因为它很棘手并且很容易出错。它经过精心设计,考虑到了正确性,并且除非您实际使用它,否则没有任何隐藏成本。
这是演示该功能的示例。
# include < sigslot/signal.hpp >
# include < string >
static int i = 0 ;
void f1 () { i += 1 ; }
void f2 () { i += 1 ; }
struct s {
void m1 () { i += 1 ; }
void m2 () { i += 1 ; }
void m3 () { i += 1 ; }
};
struct o {
void operator ()() { i += 1 ; }
};
int main () {
sigslot:: signal <> sig;
s s1;
auto s2 = std::make_shared<s>();
auto lbd = [&] { i += 1 ; };
sig. connect (f1); // #1
sig. connect (f2); // #2
sig. connect (&s::m1, &s1); // #3
sig. connect (&s::m2, &s1); // #4
sig. connect (&s::m3, &s1); // #5
sig. connect (&s::m1, s2); // #6
sig. connect (&s::m2, s2); // #7
sig. connect (o{}); // #8
sig. connect (lbd); // #9
sig (); // i == 9
sig. disconnect (f2); // #2 is removed
sig. disconnect (&s::m1); // #3 and #6 are removed
sig. disconnect (o{}); // #8 and is removed
// sig.disconnect(&o::operator()); // same as the above, more efficient
sig. disconnect (lbd); // #9 and is removed
sig. disconnect (s2); // #7 is removed
sig. disconnect (&s::m3, &s1); // #5 is removed, not #4
sig (); // i == 11
sig. disconnect_all (); // remove all remaining slots
return 0 ;
}
从版本 1.2.0 开始,可以为槽分配一个组 id,以控制槽调用的相对顺序。
同一组中槽的调用顺序未指定,不应依赖,但槽组是按组 ID 升序调用的。当一个槽的组id没有设置时,它被分配到组0。组id可以是有符号32位整数范围内的任何值。
# include < sigslot/signal.hpp >
# include < cstdio >
# include < limits >
int main () {
sigslot:: signal <> sig;
// simply assigning a group id as last argument to connect
sig. connect ([] { std::puts ( " Second " ); }, 1 );
sig. connect ([] { std::puts ( " Last " ); }, std::numeric_limits<sigslot::group_id>:: max ());
sig. connect ([] { std::puts ( " First " ); }, - 10 );
sig ();
return 0 ;
}
独立的sigslot::connect()
函数可用于将一个信号连接到另一个具有兼容参数的信号。
# include < sigslot/signal.hpp >
# include < iostream >
int main () {
sigslot:: signal < int > sig1;
sigslot:: signal < double > sig2;
sigslot::connect (sig1, sig2);
sigslot::connect (sig2, [] ( double d) { std::cout << " got " << d << std::endl; });
sig ( 1 );
return 0 ;
}
线程安全性经过单元测试。特别是,交叉信号发射和递归发射在多线程场景中运行良好。
sigslot::signal
是更通用的sigslot::signal_base
模板类的 typedef,其第一个模板参数必须是 Lockable 类型。这种类型将决定该类的锁定策略。
Sigslot 提供 2 个 typedef,
sigslot::signal
可从多个线程使用,并使用 std::mutex 作为可锁定对象。特别是,连接、断开、发射和槽执行都是线程安全的。递归信号发射也是安全的。sigslot::signal_st
是一个非线程安全的替代方案,它以安全性换取稍快的操作。 在 C++ 中,比较函数指针是一场噩梦。下面是一个表格,展示了作为展示的各种案例的大小和地址:
void fun () {}
struct b1 {
virtual ~b1 () = default ;
static void sm () {}
void m () {}
virtual void vm () {}
};
struct b2 {
virtual ~b2 () = default ;
static void sm () {}
void m () {}
virtual void vm () {}
};
struct c {
virtual ~c () = default ;
virtual void w () {}
};
struct d : b1 {
static void sm () {}
void m () {}
void vm () override {}
};
struct e : b1, c {
static void sm () {}
void m () {}
void vm () override {}
};
象征 | 海湾合作委员会 9 Linux 64 大小 | 海湾合作委员会 9 Linux 64 地址 | 微软VC 16.6 32 大小 | 微软VC 16.6 32 地址 | 海湾合作委员会 8 明格 32 大小 | 海湾合作委员会 8 明格 32 地址 | 铿锵-cl 9 32 大小 | 铿锵-cl 9 32 地址 |
---|---|---|---|---|---|---|---|---|
乐趣 | 8 | 0x802340 | 4 | 0x1311A6 | 4 | 0xF41540 | 4 | 0x0010AE |
&b1::sm | 8 | 0xE03140 | 4 | 0x7612A5 | 4 | 0x308D40 | 4 | 0x0010AE |
&b1::m | 16 | 0xF03240 | 4 | 0x1514A5 | 8 | 0x248D40 | 4 | 0x0010AE |
&b1::vm | 16 | 0x11 | 4 | 0x9F11A5 | 8 | 0x09 | 4 | 0x8023AE |
&b2::sm | 8 | 0x003340 | 4 | 0xA515A5 | 4 | 0x408D40 | 4 | 0x0010AE |
&b2::m | 16 | 0x103440 | 4 | 0xEB10A5 | 8 | 0x348D40 | 4 | 0x0010AE |
&b2::vm | 16 | 0x11 | 4 | 0x6A14A5 | 8 | 0x09 | 4 | 0x8023AE |
&d::sm | 8 | 0x203440 | 4 | 0x2612A5 | 4 | 0x108D40 | 4 | 0x0010AE |
&d::m | 16 | 0x303540 | 4 | 0x9D13A5 | 8 | 0x048D40 | 4 | 0x0010AE |
&d::虚拟机 | 16 | 0x11 | 4 | 0x4412A5 | 8 | 0x09 | 4 | 0x8023AE |
&e::sm | 8 | 0x403540 | 4 | 0xF911A5 | 4 | 0x208D40 | 4 | 0x0010AE |
&e::m | 16 | 0x503640 | 8 | 0x8111A5 | 8 | 0x148D40 | 8 | 0x0010AE |
&e::虚拟机 | 16 | 0x11 | 8 | 0xA911A5 | 8 | 0x09 | 8 | 0x8023AE |
Release模式下的MSVC和Clang-cl通过合并来优化具有相同定义的函数。可以使用/OPT:NOICF
链接器选项禁用此行为。 Sigslot 测试和示例依赖于许多相同的可调用函数来触发此行为,这就是它在受影响的编译器上停用此特定优化的原因。
在版本低于 7.4 的 GCC 中使用通用 lambda 可能会触发 Bug #68071。