对signal/slot机制非常感兴趣,所以网上找了几份实现,不过能力不足,大部分实现感觉有点难度,后来看到sigslot库,这个非常简单,核心代码其实就500来行代码。没有心理压力,所以写一份源代码分析,表示我也看懂了一份工业级别的源代码:
这是我自己画的一份sigslot的UML图片,对sigslot的源代码做了如下简化:
第二,这个UML我加上了一些自己的东西:
例如图中的has_slots和signal_base,根据UML的规则,这说明has_slots和_signal_base是一对多的关系,即一个has_slots对象可以拥有多个_signal_base对象,我添加的内容是,has_slots和_signal_base的连线,一端指向了has_slots的成员变量m_senders,这说明一个has_slots是通过m_senders这个私有变量来“拥有”多个_signal_base对象的,至于m_senders是std::set类型的,这并不是我们所关心的,所以UML图中并没有表现出来。
如果理解了sigslot的源代码,看看这个UML图,代码思路是很清晰的。现在还是让我来说明一下这个UML图吧。
因为has_slots实现了对signal对象的管理。我们来看一下如下的代码:
class A { public: void f (); void g(); void h(int); } signal0<> s1; signal0<> s2; signal1<int> s3; { A a_obj; s1.connect (&a_boj, &A::f); s2.connect (&a_obj, &A::f); s3.connect (&a_obj, &A::h); } s.emit ();
当类A的对象a_obj离开自己的作用域时,a_obj被析构。上面的代码中,类A没有继承自
has_slots,所以类A中的析构函数其实什么也没有做。也就是说,s还是连接中对象a_obj
和&A::f的连接。所以在a_obj的作用域外面调用s.emit()会导致一些问题。
由于signal和slot机制中,signal完全失去对拥有slot对象的控制权,所以,断开连接的操作,应该在类A的析构函数中完成
因为类A对象的 slot 可能连接到了许多不同的signal对象,类A的析构函数中,要自动断开所有这些signal的
连接。所以对象a中需要存储所有signal对象。代码中是采用一个set保存的:
sender_set m_senders;
这正是UML图中,has_slots和signal_base的关系为一对多的原因。
由于C++对可变模板形参没有提供支持,所以库的作者使用signal0,signal1,...,signal8,这类代码的形式来模拟多个可变形参。
对于signaln,它继承自一个_signal_basen。_signal_basen 又继承自_signal_base。_signal_base这个根基类是必须的。
如果没有_signal_base这个根基类,那么上文中的m_senders里面应该保存什么呢?signal0类的指针?signal1类的指针?
明显都不是。m_senders需要保存某个类的指针,这个指针能在运行期运行指定的函数(这个函数会需要断开slot对象和signal对象的连接)。
如上例子,类A的对象a,同时连接了signal0<>的两个对象,还连接了signal1<int>的一个对象。所以当对象a被析构时,需要断开signal0<>和signal1<int>这两个类的连接,这样m_senders中需要保存signal0<>和signal1<int>这两种类型的指针。如果没有_signal_base这个根基类,我们根本就没有办法完成这个需求。
很明显,继承,是解决这个问题的正确漂亮简单的编码方式:
class _signal_base { // 这个函数断开p_obj和signal0<>中对象的连接 virtual void slot_disconnect (has_slots<> *p_boj) = 0; } class signal0<> : public _signal_base { public: void slot_disconnect (has_slots<> *p_obj); }; class signal1<int> : public _signal_base { public: void slot_disconnect (has_slots<> *p_obj); } A::~A () { for (set_iterator beg = m_senders.begin (); beg != m_senders.end (); ++beg) { // 根据多态的规则,slot_disconnect在运行期会自动选择是 // signal0<>的slot_disconnect还是signal1<int>的 // slot_disconnect。 (*beg)->slot_disconnect (this); } }
这个关系很容易理解:每一个信号都需要包含多个slot。而slot是通过connect系列的类实现的。signal通过list容器包含了
多个connect_basen对象,实现了signal和slot之间的一对多关系。
signal通过connect系列类调用所连接的对象成员函数。connect实现了slot。
要了解connect_base类ruuhe实现slot,首先我们需要了解C++中的成员函数指针。
这里不打算深入讲解C++的成员函数指针,不过希望你能理解如下代码:
class A { public : void f(); void g (); }; typedef void (A::*AMemFunc) (); AMemFunc ptr_mem_func = &A::f; A *a = new A; a->*ptr_mem_func(); // 调用A::f () ptr_mem_func = &A::g; a->*ptr_mem_func(); // 调用A::g()
很明显,成员函数指针提供了运行期调用不同函数的功能。
为了实现slot,编译期需要记录类型信息。如下代码是不能通过编译的:
A *a; void *p = a; p->*ptr_mem_func() ;// 错误
这就是_connection0模板的作用:
通过_connection0模板,编译器记录了类型信息,使得_connection0的emit函数中才可能正确的调用函数。
不过很可惜,模板实例化出来的每个类都是不相关的,意味着,我们无法使用容器包含一下两个对象:
_connection0<A> a; _connection0<B> b;
因为a和b其实是毫无关系的两个对象,所以_connection0需要一个共同的基类:
class _connection_base0 { virtual void emit () const = 0; } template <typename T> class _connection0 : public _connection_base0 { void emit () { ... } };
这就是_connect系列中必须出现继承关系的原因。
待续,虽然sigslot的整体框架比较简单,但是代码细节中考虑到的事情还是比较多的。不过对于一个有志向研究sigslot代码的人来说,前面的分析已经足够了。