• 实现C++中的事件委托机制


    本文系原创整理,欢迎转载,请标明链接 http://www.cnblogs.com/luming1979

    有问题欢迎加qq讨论:luming_dev@qq.com

    摘要: 介绍了事件委托机制的需求,各种解决方案的演变,最终提出模板化的事件委托机制,并给出较详细的进化过程和原理说明。

    关键词: C++,委托,委托器,事件器,模板



    第一章 基础版实现

    在平时的工作中,我们经常会遇到以下情况

    void Do(int event_id)

    {

        

    }

     

    void OnEvent(int event_id)

    {

         Do(event_id);

    }

    下面是成员函数版本

    class A

    {

    public:

         void Do(int event_id)

         {

            

         }

    };

     

    class B

    {

    public:

         void OnEvent(int event_id)

         {

             a.Do(event_id);

         }

     

    private:

         A    a;

    };

     (这里a或者是B的成员,或者是全局变量,或者通过OnEvent函数传递进来)

    以上是一般情况,当B的OnEvent还需要调用另外的函数或者其他对象的函数时,就不得不对OnEvent函数作出改动,当然如果A的类型改变了,也要做相应改动,变成

    void OnEvent(int event_id)

    {

         c.Run(event_id);

    }

    或者

    void OnEvent(int event_id)

    {

         a.Do(event_id);

         c.Run(event_id);

        

    }

    由于需求的多变性,导致OnEvent函数面对不同的情况有不同的实现,类B的复用性大大降低。我们知道GUI是接收事件并作出处理的一个典型例子,如果按照以上方法,则每一种控件都需要被继承,重载OnEvent函数,用以对应不同的事件响应,是一件很可怕的任务,

    :


    第二章 多态版实现

    2.1 单任务的实现

    C++提供了多态机制,我们可以使用类的虚函数改善以上的问题。

    (在C中可以使用函数指针的方法,其本质是相同的,这个就由读者自己发挥了)

    class EventCallerBase

    {

    public:

         // 基类使用纯虚函数,派生类必须实现

         virtual void Do(int event_id) = 0;

    };

     

    class Receiver

    {

    public:

         void SetEventCaller(EventCallerBase* pCaller) { m_pCaller = pCaller; }

     

         void OnEvent(int event_id)

         {

             if (m_pCaller)

                  m_pCaller->Do(event_id);

         }

     

    private:

         EventCallerBase* m_pCaller;

    };

     

    class EventCallerA : public EventCallerBase

    {

    public:

         virtual void Do(int event_id)

         {

             printf("EventCallerA do event %d.\r\n", event_id);

         }

    };

     

    void main()

    {

         EventCallerA caller;

     

         Receiver receiver;

         receiver.SetEventCaller(&caller);

        

         receiver.OnEvent(99);

    }

    输出:EventCallerA do event 99.

    2.2 多任务的实现

    对于需要对多个对象调用其函数的情况,用以下方式

    EventCallerBase,EventCallerA的实现同上

    class EventCallerB : public EventCallerBase

    {

    public:

         virtual void Do(int event_id)

         {

             printf("EventCallerB do event %d.\r\n", event_id);

         }

    };

     

    class Receiver

    {

    public:

         void AddEventCaller(EventCallerBase* pCaller)

         {

             if (pCaller)

                  m_CallerList.push_back(pCaller);

         }

     

         void OnEvent(int event_id)

         {

             list<EventCallerBase*>::iterator it = m_CallerList.begin();

             while (it != m_CallerList.end())

             {

                  EventCallerBase* pCaller = *it;

                  if (pCaller)

                       pCaller->Do(event_id);

                  ++it;

             }

         }

     

    private:

         list<EventCallerBase*> m_CallerList;

    };

     

    void main()

    {

         EventCallerA callerA;

         EventCallerB callerB;

     

         Receiver receiver;

         receiver.AddEventCaller(&callerA);

         receiver.AddEventCaller(&callerB);

        

         receiver.OnEvent(99);

    }

    输出:EventCallerA do event 99.

          EventCallerB do event 99.

    在以上方法中,类Receiver基本做到了重用,除了OnEvent参数类型和个数的改变,一般情况下,当有事件发生,调用不同的事件处理函数时,只要继承EventCallerBase类,实现Do函数,并在初始阶段设定AddEventCaller即可。这种方法在GUI中,已经能尽可能地重用发生事件部分的类和代码,把主要工作放在实现事件响应的处理上。

    2.3 对已有类的改造

    这里有个问题,如果有一个需求,比如窗口最大化,需要调用成员函数System::Maximize(),怎么办?类System是一个既有类,不能随便改动,来继承EventCallerBase。上面的方法岂不是不实用?

    小小地动动脑筋,方法是有的:

    class System

    {

    public:

         void Maximize(void)   { printf("Window is maximized.\r\n"); }

    };

     

    class EventCallerSystem : public EventCallerBase

    {

    public:

         EventCallerSystem(System* pSystem) { m_pSystem = pSystem; }

     

         virtual void Do(int event_id)

         {

             if (m_pSystem)

                  m_pSystem->Maximize()

         }

     

    private:

         System* m_pSystem;

    };

     

    void main()

    {

         System system;

         EventCallerSystem callerSystem(&system);

     

         Receiver receiver;

         receiver.AddEventCaller(&callerSystem);

        

         receiver.OnEvent(99);

    }

    输出:Window is maximized.

    解决了问题,还留了一个小尾巴,就是要多实现一个EventCallerSystem类。

    有没有办法把这个小尾巴也一并解决掉呢,这就到了这篇文章的主题――C++中的事件委托机制,这次我们用到了C++的另一个特性---模板。


    第三章 事件委托版实现

    3.1 函数指针的使用

    我们首先复习一下函数指针和成员函数指针。

    3.1.1函数指针

    在C和C++语言中,一个命名为my_func_ptr的函数指针指向以一个int和一个char*为参数的函数,这个函数返回一个浮点值,声明如下:

    float (*my_func_ptr)(int, char *);

    为了便于理解,一般我们使用typedef关键字。

    typedef float (*MyFuncPtrType)(int, char *);

    如果你的函数指针指向一个型如float some_func(int, char *)的函数,这样做就可以了:

    MyFuncPtrType my_func_ptr = some_func;

    当你想调用它所指向的函数时,可以这样写:

    (*my_func_ptr)(7, "HelloWorld");

    或者

    my_func_ptr(7, "HelloWorld");

    3.1.2成员函数指针

    在C++程序中,很多函数是成员函数,即这些函数是某个类中的一部分。你不可以像一个普通的函数指针那样指向一个成员函数,正确的做法应该是,你必须使用一个成员函数指针。一个成员函数的指针指向类中的一个成员函数,并有相同的参数,声明如下:

    float (SomeClass::*my_memfunc_ptr)(int, char *);

    将函数指针指向型如float SomeClass::some_member_func(int, char *)的函数,可以这样写:

        my_memfunc_ptr = &SomeClass::some_member_func;

    当你想调用它所指向的成员函数时,可以这样写:

    SomeClass* x = new SomeClass;

    (x->*my_memfunc_ptr)(6, "HelloWorld");


    3.2 函数指针的大小

    class A

    {

    public:

         int Afunc() { return 2; };

    };

     

    class B

    {

    public:

         int Bfunc() { return 3; };

    };

     

    class D: public A, public B

    {

    public:

         int Dfunc() { return 5; };

    };

     

    int main()

    {

         printf("%d\n", sizeof(&main));

         printf("%d\n", sizeof(&A::Afunc));

         printf("%d\n", sizeof(&B::Bfunc));

         printf("%d\n", sizeof(&D::Dfunc));

         return 0;

    }

    输出:

    4

    4

    4

    8

    可以看出,普通函数的指针大小是4,

    普通类的成员函数的指针大小也是4,

    对于多重继承的类,成员函数的指针大小是8,

    还有成员函数指针大小是12和16的情况,在这里就不展开了。

    (需要特别注意的是,相同的代码,在不同的编译器下,函数指针的大小也不相同)。

    对函数指针和成员函数指针的复习就到这里。


    3.3 C++中的事件委托

    以下登场的是本文的主角:模板化实现的C++中的事件委托

    3.3.1代码

    /////////////////////////////////////////////////////////////////////////////////

    /// \class FuncCache

    /// \brief 函数对象寄存器

    /////////////////////////////////////////////////////////////////////////////////

    template <typename ReturnType>

    class FuncCache

    {

         static const int SIZE = 48;

         typedef ReturnType (*func_caller)(FuncCache*);

     

         /// \class MemberFuncAssist

         /// \brief 对象成员函数寄存器的辅助器

         class FuncCacheAssist

         {

         public:

             /// \brief 构造函数,初始化。

             FuncCacheAssist(FuncCache* pFunc)

             {

                  m_Size = 0;

                  m_pFunc = pFunc;

                  // 读取用偏移必须归位

                  m_pFunc->m_Cur = 0;

             }

             /// \brief 析构函数。

             ~FuncCacheAssist(void)

             {

                  // 弹出以前压入的参数

                  if (m_Size > 0)

                       m_pFunc->Pop(m_Size);

             }

             /// \brief 压入指定大小的数据。

             uint Push(const void* pData, uint size)

             {

                  m_Size += size;

                  return m_pFunc->Push(pData, size);

             }

     

             /// 压入参数的大小

             int                m_Size;

             /// 对象成员函数寄存器

             FuncCache*         m_pFunc;

         };

     

    public:

         /// \brief 构造函数,初始化。

         FuncCache(func_caller func)

         {

             m_Size = 0;

             m_Cur = 0;

             m_Func = func;

         }

         /// \brief 压入指定大小的数据。

         uint     Push(const void* pData, uint size)

         {

             size = (size <= SIZE - m_Size)? size : (SIZE - m_Size);

             memcpy(m_Buffer + m_Size, pData, size);

             m_Size += size;

             return size;

         }

         /// \brief 弹出指定大小的数据。

         uint      Pop(uint size)

         {

             size = (size < m_Size)? size : m_Size;

             m_Size -= size;

             return size;

         }

         /// \brief 读取指定大小的数据,返回指针。

         void*         Read(uint size)

         {

             m_Cur += size;

             return (m_Buffer + m_Cur - size);

         }

         /// \brief 执行一个参数的函数。

         ReturnType    Execute(const void* pData)

         {

             // 用辅助结构控制

             FuncCacheAssist assist(this);

             // 压入参数

             assist.Push(&pData, sizeof(void*));

             // 执行函数

             return m_Func(this);

         }

     

    protected:

         /// 对象,函数,参数指针的缓冲区

         uchar         m_Buffer[SIZE];

         /// 缓冲区大小

         uint          m_Size;

         /// 缓冲区读取用的偏移

         uint          m_Cur;

         /// 操作函数的指针

         func_caller   m_Func;

    };

     

     

    /////////////////////////////////////////////////////////////////////////////////

    /// \class MFuncCall_1

    /// \brief 一个参数的成员函数执行体

    /////////////////////////////////////////////////////////////////////////////////

    template <typename ReturnType, typename Caller, typename Func, typename ParamType>

    class MFuncCall_1

    {

    public:

         /// \brief 执行一个参数的成员函数。

         static ReturnType MFuncCall(FuncCache<ReturnType>* pMFunc)

         {

             // 获得对象指针

             Caller* pCaller = *(Caller**)pMFunc->Read(sizeof(Caller*));

             // 获得成员函数指针

             Func func = *(Func*)pMFunc->Read(sizeof(Func));

             // 获得参数的指针

             ParamType* pData = *(ParamType**)pMFunc->Read(sizeof(ParamType*));

             // 执行成员函数

             return (pCaller->*func)(*pData);

         }

    };

    /////////////////////////////////////////////////////////////////////////////////

    /// \class L_SignalRoot

    /// \brief 类型检查严格的事件委托器基类

    /////////////////////////////////////////////////////////////////////////////////

    template <typename ReturnType>

    class L_SignalRoot

    {

    public:

         /// \brief 指定事件名,卸载指定对象的事件委托器。

         template <typename Caller>

         void     MFuncUnregister(Caller* pCaller)

         {

             func_map& func_list = m_MemberFuncMap;

             func_map::iterator it = func_list.find(pCaller);

             if (it != func_list.end())

                  func_list.erase(it);

         }

         /// \brief 清空所有事件委托器。

         void     MFuncClear(void)

         {

             m_MemberFuncMap.clear();

         }

     

    protected:

         typedef map< void*, FuncCache<ReturnType> > func_map;

         /// 事件名和绑定的事件委托器的列表

         func_map m_MemberFuncMap;

    };

     

     

    /////////////////////////////////////////////////////////////////////////////////

    /// \class L_Signal_1

    /// \brief 类型检查严格,一个参数的事件委托器

    /////////////////////////////////////////////////////////////////////////////////

    template <typename ReturnType, typename ParamType>

    class L_Signal_1 : public L_SignalRoot<ReturnType>

    {

    public:

         /// \brief 指定事件名,注册对应的一个参数的事件委托器。

         template <typename Caller, typename Func>

         void     MFuncRegister(Caller* pCaller, Func func)

         {

             // 指定专门处理一个参数的函数执行体

             FuncCache<ReturnType> mfunc(MFuncCall_1<ReturnType, Caller, Func, ParamType>::MFuncCall);

             // 压入对象和函数

             mfunc.Push(&pCaller, sizeof(Caller*));

             mfunc.Push(&func, sizeof(Func));

             // 添加到事件委托器列表

             m_MemberFuncMap.insert(make_pair(pCaller, mfunc));

         }

         /// \brief 指定事件名,调用其对应的一个参数的事件委托器。

         ReturnType    MFuncCall(const ParamType& data)

         {

             // 清空返回值

             ReturnType result;

             memset(&result, 0, sizeof(result));

             // 对于所有委托器,调用注册的函数

             func_map::iterator it = m_MemberFuncMap.begin();

             while (it != m_MemberFuncMap.end())

             {

                  result = it->second.Execute(&data);

                  ++it;

             }

             return result;

         }

    };

     

     

    class EventCallerA

    {

    public:

         bool Do(int event_id)

         {

             printf("EventCallerA do event %d.\r\n", event_id);

             return true;

         }

    };

    class EventCallerB

    {

    public:

         bool Run(int event_id)

         {

             printf("EventCallerB run event %d.\r\n", event_id);

             return true;

         }

    };

     

    void main()

    {

         // 申明返回值是bool类型,参数是int类型,单参数的事件器

         L_Signal_1<bool, int> signal;

         EventCallerA callerA;

         EventCallerB callerB;

    // 注册委托器并调用事件

         signal.MFuncRegister(&callerA, &EventCallerA::Do);

         signal.MFuncRegister(&callerB, &EventCallerB::Run);

         signal.MFuncCall(1);

    }

    注意这里EventCallerA和EventCallerB并没有相同的基类。

    3.3.2名词定义

    先定义一些概念,便于我们统一理解

    事件器:指发生事件后,处理事件的响应,逐个通知事先注册的对象。

    委托器:指某事件发生后,需要被通知,并执行事先注册的函数的对象。

    3.3.3需求

    再谈谈我们的需求:

    1.     某个事件发生后,能通知到所有事先注册过的委托器。

    2.     委托器的类型可能千差万别。

    3.     加入参数使这个机制更灵活,应对每次不同的事件参数,支持1个,2个,甚至更多的参数。

    4.     参数的类型也不希望有限制。

    5.     委托器有执行结果,可以被事件器获取。

    6.     委托器销毁的时候,需要通知事件器,将其从委托器列表中排除。

    3.3.4限制

    最后谈谈可以有的限制

    1.     针对同一个事件,委托器的函数参数类型应该是相同的,顺序也相同,因为事件的参数类型是不变的,否则可以分解为两个事件。由事件起始,通知委托器,如果参数类型各不相同,没有意义。

    2.     函数参数过多也没有意义,因为我们知道,多个参数的需求可以用类或者结构体代替,以减少参数个数。

    3.     大多数情况下,我们只需要知道最后一个委托器的执行结果。

    3.3.5解决方法

    怎么办?抽象!

    如何抽象??往二进制层面抽象!!

    当我们要统一处理一些需求的时候,我们只有把需求看成相同的类型和格式。

    说到底,对象只是内存中的一块数据,而函数也是内存中的一段数据,我们可以用内存地址的方式来统一表示它们。

    3.3.6代码解析

    模板类FuncCache 就是为了实现这一级的抽象而存在,以下对FuncCache做必要解析:

    FuncCache::m_Buffer用来存储对象的指针,成员函数的指针,以及函数参数的指针,暂定大小为固定48字节,其中对象指针4字节,成员函数指针4到16字节不等,参数指针每个为4字节,可以有多个。

    FuncCache::m_Size表示目前用到了多少字节的数据。

    FuncCache::m_Cur表示用来从头依次读取对象指针,成员函数指针和参数指针的数据偏移量。

    FuncCache::m_Func很关键,因为以上都是数据,光有材料还要明确如何处理,其就承担了这个重要的任务,类型是typedef ReturnType (*func_caller)(FuncCache*),对于同种ReturnType,其类型是固定的,这是很关键的一步,完成了从不同类型的对象,不同类型的函数以及参数(有条件的)到一致的对象之间的抽象。

    FuncCache的各个函数很简单,不做详细说明了,值得一提的是其内部类FuncCacheAssist,这个内部类存在的主要价值是在Execute退出的时候,将压入的参数排除。

    接下来解析模板类MFuncCall_1

    这里只是列了对于一个参数的函数委托器的实现,无参数、多参数的实现类似,请读者自行发挥。

    该模板类很简单,只有一个函数,但是提供了很灵活的功能,返回类型,对象类型,成员函数类型,参数类型,全部可以自定义的,也只有通过模板类才能实现所需要的功能,具体函数算法很简单,就不展开了。

    最后是模板类L_SignalRoot和L_Signal_1,也就是事件器。

    ReturnType是模板化的返回类型,ParamType是模板化的单参数类型。

    该类只有一个变量m_MemberFuncMap,用来保存所有的委托器的抽象,也就是FuncCache

    该类提供了四个接口,也是对于使用者最常使用的:

    1.       MFuncRegister  注册委托器对象和函数

    2.       MFuncCall       调用所有注册的委托器,并返回最后调用的结果。

    3.       MFuncUnregister 根据对象指针,删除注册过的委托器,委托器在事件器之前销毁的话,             必须调用这个接口,这点没有做成自动的,主要为了减少类的复杂度。

    4.       MFuncClear      清空所有委托器。

    给用户的接口很简单明了易于使用。

    参考

    《成员函数指针与高性能的C++委托》 Don Clugston

    有问题欢迎加qq讨论:luming_dev@qq.com

  • 相关阅读:
    使用C#实现DHT磁力搜索的BT种子后端管理程序+数据库设计(开源)
    便携版WinSCP在命令行下同步文件夹
    ffmpeg (ffprobe)分析文件关键帧时间点
    sqlite删除数据或者表后,回收数据库文件大小
    ubuntu 20.04下 freeswitch 配合 fail2ban 防恶意访问
    ffmpeg使用nvenc编码的结论记录
    PC版跑跑卡丁车 故事模式 亚瑟传说章节 卡美洛庆典 2阶段 心灵之眼 攻略
    There was an error loading or playing the video
    Nvidia RTX Voice 启动报错修复方法
    火狐浏览器 关闭跨域限制
  • 原文地址:https://www.cnblogs.com/luming1979/p/1963750.html
Copyright © 2020-2023  润新知