sigslot源代码分析

对signal/slot机制非常感兴趣,所以网上找了几份实现,不过能力不足,大部分实现感觉有点难度,后来看到sigslot库,这个非常简单,核心代码其实就500来行代码。没有心理压力,所以写一份源代码分析,表示我也看懂了一份工业级别的源代码:

sigslot源代码分析_第1张图片

这是我自己画的一份sigslot的UML图片,对sigslot的源代码做了如下简化:

  1. 消除了signal1...signaln之类的重复代码,这样子就消除了connect1...connectn这些“多余”的代码了。这样简化了很多。
  2. 去除了多线程管理代码,这个在分析源代码的时候没有什么作用。

第二,这个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图中并没有表现出来。

解释一下这个UML图

如果理解了sigslot的源代码,看看这个UML图,代码思路是很清晰的。现在还是让我来说明一下这个UML图吧。

为什么需要has_slots类?为了什么有slot的类要继承自has_slots类呢?

因为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的关系为一对多的原因。

关于_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); 
    }
}

_connect_base0和_signal_base0之间的多对一关系


这个关系很容易理解:每一个信号都需要包含多个slot。而slot是通过connect系列的类实现的。signal通过list容器包含了
多个connect_basen对象,实现了signal和slot之间的一对多关系。

signal通过connect系列类调用所连接的对象成员函数。connect实现了slot。

connect_base类的继承关系和connect_base类如何实现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代码的人来说,前面的分析已经足够了。

你可能感兴趣的:(源代码研究,sigslot,C++信号槽)