我是靠谱客的博主 慈祥茉莉,这篇文章主要介绍从零开始实现信号槽机制:一,现在分享给大家,希望可以做个参考。

我们从一个具体的问题入手:

现在有一堆按钮,以及一堆电器,按钮对它需要控制的对象一无所知,电器也不知道它们开关的具体类型,它们之间的关系可能是一对多,也可能是多对一,并且需要支持动态添加和删除,应该如何设计这个结构?

这里有个形象的图:



基于静态成员函数的回调

--------------------------------------------------------------------------------

为了实现组件间的控制,我们很容易想到“回调函数”,对于C++开发者,我们肯定不希望一个类自身的处理函数存在于类外,但是类成员函数中被自动添加的隐形this形参造成了函数指针调用的不匹配,于是我们想到了使用static成员函数:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 被调类 class Tv { public: static void onBtnClicked(bool b) { if ( b == true ) cout << "Tv is being turned on."; else cout << "Tv is being turned off."; } }; // 还有其他可能被调用的对象 // class Lamp... // class Laptop... typedef void (*PF)(bool); class Button { public: //主调函数 void click(PF p, bool b) { p(b); } }; int main() { PF p = &Tv::onBtnClicked; Button btn; btn.click(p, true); return 0; }

这样我们就可以动态地为Button赋上它需要调用那个函数并执行。但缺点也是显而易见的——被调函数作为静态函数不能访问非静态成员与函数,Button触发的结果对于每一个Tv对象都是一样的,这显然不是我们想要的。因此,我们想到了在Button中动态地传入被调对象的指针,这样,通过这个指针我们就可以调用该对象的非静态成员函数了。由于Button还需要控制其他设备,我们很自然地想到使用模板:


基于非静态成员函数的回调

--------------------------------------------------------------------------------

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//被调类 class Tv { public: Tv(int t1, int t2) : bootTime(t1), offTime(t2){} //被调函数 void onBtnClicked(bool b) { if ( b == true ) cout << "Tv is being turned on. bootTime is " << bootTime; else cout << "Tv is being turned off. offTime is " << offTime; } private: int bootTime; int offTime; }; // 还有其他可能被 Button 类控制的被调类 class Lamp { public: void onBtnClicked() { cout << "This Lamp is control by voice"; } }; // class Laptop... // 主调类 template<typename Tobject, typename Tparam> class Button { typedef void (Tobject::*Tfunc)(Tparam); public: Button(Tobject* pInstance, Tfunc p) { m_pInstance = pInstance; m_pf = p; } //主调函数 void click(Tparam p) { (m_pInstance->*m_pf)(p); } private: Tfunc m_pf; Tobject* m_pInstance; }; int main() { Tv tv1(20, 40); Button<Tv, bool> btn(&tv1, &Tv::onBtnClicked); btn.click(true); return 0; }


这样看起来还不错,我们不仅将被调对象的类型独立于Button,并且参数类型也是可变的(当然为了满足一对多的关系,Button应该维护Tobject*和Tfunc的List。)。但我们依然没有做到完全解耦:一、对于Button类中声明的函数指针( typedef void (Tobject::*Tfunc)(Tparam) ),被调函数的返回值与参数数量必须与其完全一致,但多个类型的被调函数可能不尽相同,例如Lamp::onBtnClicked();二、Button类掌握着所有被调对象的指针,这并不是一件安全的事情;三、被调对象先于调用被析构,将得到错误的结果。我们可以将main()中代码改成下面这样:

复制代码
1
2
3
4
5
6
7
8
int main() { Tv *tv1 = new Tv(20, 40); Button<Tv, bool> btn(tv1, &Tv::onBtnClicked); delete tv1; btn.click(true); return 0; }

危险的是,这是一个NULL指针调用成员函数的问题,很多时候编译器并不对该情况报错,于是我们得到了一个诡异的结果——打印了一个随机的bootTime值。有时这种情况藏得很隐蔽,它可能将你拖入调试bug的沼泽。之所以出现这样的情况,是因为Tv类的完全独立性,它没有能力将自身被delete的信息告诉Button,从而导致Button对其固执地调用;而Button类过多地关注被调对象与其函数指针也依然存在耦合。但是Button类的设计要想做到完全与被调类无关似乎是一件不可能的事情,否则对方如何知道这个函数触发了呢?


尝试使用信号槽

--------------------------------------------------------------------------------

现在我们希望Button类在设计时能够像下面这样保持自身的简洁与独立性,它只负责发出指令就好:

复制代码
1
2
3
4
5
class Button { signals: void click(bool); };


显然,为了解耦,我们不想在 click() 中做调用操作,那么整个类中唯一可以做文章的地方似乎就是这个signals如果让这个signals成为一个类,而click作为一个对象的话,那可发挥的余地就大多了。好的,让我们来尝试这个新的结构。首先我们把Button类中负责调用其他函数的内容剥离出来,用一个新的类Connection表示:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template<typename Tparam> class Connection_Base { public: virtual MySlot* getdest() const = 0; virtual void emitSignal(Tparam) = 0; virtual ~Connection_Base(){} }; template<typename Tobject, typename Tparam> class Connection : public Connection_Base<Tparam> { typedef void (Tobject::*Tfunc)(Tparam); public: Connection(Tobject* pobject, Tfunc p) { m_pobject = pobject; m_pf = p; } void emitSignal(Tparam parm) { (m_pobject->*m_pf)(parm); } MySlot* getdest() const { return m_pobject; } private: Tobject* m_pobject; Tfunc m_pf; };

注意到这里我们使用了两个类,Connection和它的抽象基类Connection_Base,Connection所做的工作与我们从Button类中剥离出来的差不多,同样维护着一个对象及其函数指针。之所以要添加这个基类Connection_Base是因为我们在发出信号时是不关心接受者的类型的,因此只了解参数类型的信号对象就可以只与Connection_Base进行交互。


槽基类需要解决的问题

--------------------------------------------------------------------------------

接下来,为了让“电器们”在销毁时能够及时解除它身上所有的绑定,最直接的方式是通知所有的主调类将它删除。我们可以设计一个父类来完成这个工作,拥有槽函数的类只需要继承它就好:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MySlot { private: typedef std::set<_Signal_Base*> sender_set; typedef sender_set::const_iterator const_iterator; public: void signal_connect(_Signal_Base* sender) { m_senders.insert(sender); } void signal_disconnect(_Signal_Base* sender) { m_senders.erase(sender); } virtual ~MySlot() { disconnect_all(); } void disconnect_all() { const_iterator it = m_senders.begin(); const_iterator itEnd = m_senders.end(); while(it != itEnd) { (*it)->slot_disconnect(this); ++it; } m_senders.erase(m_senders.begin(), m_senders.end()); } private: sender_set m_senders; };

即使现在我们还没贴出_Signal_Base的代码,相信 MySlot 这个类也依然十分容易理解,让我们把每个信号理解为一个对象,那么 MySlot 则维护着一个存储着所有当前已绑定的信号的集合,在析构函数中我们调用disconnect_all(),调用每个信号对象的slot_disconnect()函数移除自身。


Signal_Base维护Connection列表, MySignal是个functor

--------------------------------------------------------------------------------

有了联结者和槽,接下来我们就该实现Signal了:

复制代码
1
2
3
4
5
class _Signal_Base { public: virtual void slot_disconnect(MySlot* pslot) = 0; };
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
template<typename Tparam> class Signal_Base : public _Signal_Base { public: typedef std::list<Connection_Base<Tparam>*> connections_list; ~Signal_Base() { disconnect_all(); } void disconnect_all() { typename connections_list::const_iterator it = m_connected_slots.begin(); typename connections_list::const_iterator itEnd = m_connected_slots.end(); while(it != itEnd) { (*it)->getdest()->signal_disconnect(this); delete *it; ++it; } m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); } void disconnect(MySlot* pclass) { 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); pclass->signal_disconnect(this); return; } ++it; } } void slot_disconnect(MySlot* pslot) { typename connections_list::iterator it = m_connected_slots.begin(); typename connections_list::iterator itEnd = m_connected_slots.end(); while(it != itEnd) { typename connections_list::iterator itNext = it; ++itNext; if((*it)->getdest() == pslot) { m_connected_slots.erase(it); // delete *it; } it = itNext; } } protected: connections_list m_connected_slots; };

Signal_Base类维护了一个Connection的列表,由于每个新的链接都基于一个Connection,通过Connection的getdest()函数可以确定该链接绑定在哪个对象上,这样,删除某个Connection就可以删除特定的链接了。最后的MySignal类是酱紫的:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template<typename Tparam> class MySignal : public Signal_Base<Tparam> { public: template<typename Tobject> void connect(Tobject* pclass, void (Tobject::*pmemfun)(Tparam)) { Connection<Tobject, Tparam>* conn = new Connection<Tobject, Tparam>(pclass, pmemfun); Signal_Base<Tparam>::m_connected_slots.push_back(conn); pclass->signal_connect(this); } void operator()(Tparam p) { typename std::list<Connection_Base<Tparam>*>::const_iterator itNext, it = Signal_Base<Tparam>::m_connected_slots.begin(); typename std::list<Connection_Base<Tparam>*>::const_iterator itEnd = Signal_Base<Tparam>::m_connected_slots.end(); while(it != itEnd) { itNext = it; ++itNext; (*it)->emitSignal(p); it = itNext; } } };

MySignal提供了connect()函数用来为新的链接生成Connection对象,然后重载了operator()使得我们可以使用“signal()”这种类似函数的形式来发出信号。


测试一下^_^

--------------------------------------------------------------------------------

一切就绪,写段测试代码看看:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Button { public: MySignal<bool> click; void nowClick(bool b) { click(b); } }; class Tv : public MySlot { public: Tv(int i) : m_id(i){} void onButtonClicked(bool b) { if ( b == true ) cout << "Tv " << m_id << " is being turned on. " << endl; else cout << "Tv "<< m_id <<" is being turned off. " << endl; } private: int m_id; }; int main() { Button btn; Tv *tv1 = new Tv(1); Tv *tv2 = new Tv(2); btn.click.connect(tv1, &Tv::onButtonClicked); btn.click.connect(tv2, &Tv::onButtonClicked); delete tv1; btn.nowClick(false); return 0; }

可以看到,Button类和Tv类现在可以彻底不再相互关心,而链接的工作交给客户程序(main)去做,同时,在delete tv1之后,我们也不会碰到异常,因为tv1在析构时就调用Signal对象将自身的Connection删除了。完整的代码实际就是把上面这几块揉在一起,就不再贴了,有兴趣的话可以到博主的百度网盘下载:http://pan.baidu.com/s/1i3F8gkd


可能你已经注意到了,如果我们的信号有两个参数呢?是的,我们需要再写一个有两个参数的Signal_Base,Connection和MySignal。。。如果我们分别为0-8个参数写了9个版本,并为这其中的容器操作加上线程安全机制。。。那么我们现在完成的就是一份大名鼎鼎的 sigslot 库啦~^_^

不知不觉篇幅有点长了~我们就留到下篇(从零开始实现信号槽机制:二)再来探讨下,Qt中信号槽的实现与sigslot又有哪些差异?以及moc和QMetaObject又给信号槽机制带来了怎样的灵活性~


最后

以上就是慈祥茉莉最近收集整理的关于从零开始实现信号槽机制:一的全部内容,更多相关从零开始实现信号槽机制内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(103)

评论列表共有 0 条评论

立即
投稿
返回
顶部