从零开始实现信号槽机制:一

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

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

这里有个形象的图:

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

// 被调类
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还需要控制其他设备,我们很自然地想到使用模板:

//被调类
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()中代码改成下面这样:

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类中负责调用其他函数的内容剥离出来,用一个新的类Connection表示:

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进行交互。

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

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了:

class _Signal_Base
{
public:
    virtual void slot_disconnect(MySlot* pslot) = 0;
};
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类是酱紫的:

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()”这种类似函数的形式来发出信号。

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

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之后,我们也不会碰到异常。完整的代码实际就是把上面这几块揉在一起,就不再贴了,有兴趣的话可以到博主的百度网盘下载:http://pan.baidu.com/s/1i3F8gkd

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

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

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-28 11:03:35

从零开始实现信号槽机制:一的相关文章

深入理解信号槽机制(一)(大局观,讲的不错)

这篇文章来自于 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19.需要说明的是,我们这里所说的“信号槽”不仅仅是指 Qt 库里面的信号槽,而是站在一个全局的高度,从系统的角度来理解信号槽.所以在这篇文章中,Qt 信号槽仅仅作为一种实现来介绍,我们还将介绍另外一种信号槽的实现——boost::signal.因此,当你在文章中看到一些信号的名字时,或许仅仅是为了描述方便而杜撰的,实际并没有这个信号. 什么是信号槽? 这个问题我们可

Qt开发之信号槽机制

一.信号槽机制原理 1.如何声明信号槽 Qt头文件中一段的简化版: class Example: public QObject { Q_OBJECT signals: void customSignal(); void customSignal(int i) public slots: void customSlot(); void customSlot(int i); }; 2.宏与MOC源对象 摘录代码: // qobjectdefs.h i. … ii. #define slots iii

Qt高级——Qt信号槽机制源码解析

Qt高级--Qt信号槽机制源码解析 基于Qt4.8.6版本 一.信号槽机制的原理 1.信号槽简介 信号槽是观察者模式的一种实现,特性如下:A.一个信号就是一个能够被观察的事件,或者至少是事件已经发生的一种通知:B.一个槽就是一个观察者,通常就是在被观察的对象发生改变的时候--也可以说是信号发出的时候--被调用的函数:C.信号与槽的连接,形成一种观察者-被观察者的关系:D.当事件或者状态发生改变的时候,信号就会被发出:同时,信号发出者有义务调用所有注册的对这个事件(信号)感兴趣的函数(槽).信号和

非Qt工程使用Qt的信号槽机制

非Qt工程,使用Qt的信号槽机制,蛋疼不?反正我现在就是要做这样一件蛋疼的事. 要使用Qt的信号槽机制,下面是从Qt Assist里面关于 signal & slots 的一句介绍: All classes that contain signals or slots must mention Q_OBJECT at the top of their declaration. They must also derive (directly or indirectly) from QObject.

[转]深入理解信号槽机制

原文不可考 来源链接http://blog.csdn.net/liuuze5/article/details/53523463 深入理解信号槽(一) 这篇文章来自于 A Deeper Look at Signals and Slots,Scott Collins 2005.12.19.需要说明的是,我们这里所说的"信号槽"不仅仅是指 Qt 库里面的信号槽,而是站在一个全局的高度,从系统的角度来理解信号槽.所以在这篇文章中,Qt 信号槽仅仅作为一种实现来介绍,我们还将介绍另外一种信号槽的

PyQt5快速入门(二)PyQt5信号槽机制

PyQt5快速入门(二)PyQt5信号槽机制 一.信号槽机制简介 1.信号槽简介 信号槽是Qt的核心机制,也是PyQt编程中对象进行通信的机制.在Qt中,QObject对象和PyQt中所有继承自QWidget的控件都支持信号槽机制.当信号发射时,连接的槽函数会自动执行.在PyQt5中,信号与槽函数通过object.signal.connect()方法进行连接.信号槽特点如下:(1)一个信号可以连接多个槽(2)一个信号可以连接另一个信号(3)信号参数可以是任意Python类型(4)一个槽可以监听多

Qt信号槽机制源码解析

Qt信号槽机制源码解析 来源 https://blog.51cto.com/9291927/2070398 一.信号槽机制的原理 1.信号槽简介 信号槽是观察者模式的一种实现,特性如下:A.一个信号就是一个能够被观察的事件,或者至少是事件已经发生的一种通知:B.一个槽就是一个观察者,通常就是在被观察的对象发生改变的时候——也可以说是信号发出的时候——被调用的函数:C.信号与槽的连接,形成一种观察者-被观察者的关系:D.当事件或者状态发生改变的时候,信号就会被发出:同时,信号发出者有义务调用所有注

信号槽机制的心得

使用信号和槽机制,要注意以下问题: 1.信号和槽的机制是非常有效的,但是它不像“真正的”回调那样快.信号和槽稍微有些慢,这是因为它们所提供的灵活性.但这种损失相对来说是比较小的.但要追求高效率的话,比如在实时系统中就要尽量少用这种机制. 2.信号和槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时有可能形成死循环,所以,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射所接收到的同样的信号. 3.如果一个信号和多个槽相关联的话,那当这个信号被发射时,与之相关联的槽的执行顺序将

PyQt5信号-槽机制

signal -> emit -> slot signal.connect(slot) signal.disconnect(slot) 信号 (singal) 可以连接无数多个槽 (slot),或者没有连接槽也没有问题,信号也可以连接其他的信号. 连接的基本语句形式如下: who.singal.connect(slot) 信号是 QObject 的一个属性.只有通过 connect方法连接起来,信号-槽机制就建立起来了.类似的信号还有 disconnect 方法和emit 方法. discon