65.9K
CodeProject 正在变化。 阅读更多。
Home

MappedHandler - 那个可怕的 hack!或成员函数的 Functor

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (8投票s)

2006 年 8 月 15 日

6分钟阅读

viewsIcon

21242

downloadIcon

498

一篇关于另一个标准的 C++ 委托式实现的类,用于将句柄映射到类的非静态成员函数。

引言

CodeProject 上目前有许多文章提供了 C# 委托的标准 C++ 版本。它们大多数的区别在于作者在开发时所需的不同。有些是为了速度;有些是为了符合标准;还有些则侧重于可移植性,但几乎所有人都希望有一种通用的方法将类的成员函数映射到一个事件。

我开始走这条路是因为我想创建一个自己的框架,用于通过一个通用的 API 在 C++ 中为 Linux 和 Windows 构建应用程序。是的,我知道市面上有很多努力都接近这个目标,但实际上我所寻求的是在一个框架中拥有我需要的一切并实现集成。集成到足以让窗口系统(如 Win32 消息泵)或通信层(如 TCP/IP)生成的事件能够由同一个类以标准化的方式处理。因此,为了教学经验以及真正拥有一个符合我思维方式的东西,我决定自己构建。

所以,我们首先需要一个句柄来响应框架内的事件。我设计的系统使用比静态回调更多的对象,因此我只需要一种方法将类对象的成员函数映射到一个轻量级对象,基本上就是一个 functor,用于事件系统。因此,这个初始实现尚未经过测试或增强以支持静态成员函数或标准函数。如果人们实际开始使用并提出请求,我会添加它们。

背景

我没有对这段代码进行性能分析,所以我不知道它的性能如何。为了保证性能,我会参考 Dan Clugston 和/或 Sergey Ryazanov 的作品,如果这个实现被证明效率低下的话。他们还涵盖了不同编译器 bug 以及标准与可移植性问题所涉及的所有细微之处。我提供的是 Dan 所称的“可怕的 hack”的封装版本。

这段代码使用了 VC++ 6.0 和 VS 2003 以及 Fedora Core 5 上的 GCC 4.1.1 进行测试。句柄使用了 MFC 使用的方法,该方法使用联合体来掩盖类类型,当将函数指针转换为不同类型时。这是 Dan 所提到的那个可怕的 hack,我试图通过将其封装在模板中来减少对 C++ 类型系统的肆无忌惮的破坏。最初是在 VC++ 中开发的,没有启用任何扩展。不需要 RTTI,除了测试应用程序需要的 tchariostream 之外,不需要任何其他头文件。

我的目标是易于使用。我研究了 Boost 中的 slots 和 signals,但它们并不吸引我。主要是因为它们的语义与我所寻求的不符。在我构建此代码时(2004 年初),我并不知道 Dan 的 Fast Delegate,否则我可能已经使用了它,现在也可能使用。我需要的是能够基于函数签名声明一个句柄类型,并将其用作 functor。我并没有过多地担心它是否能照顾到客户端开发者,它期望的是负责任的使用。但是,它将其使用限制在常见的接口上,例如 MFC 用于映射到线程和窗口类的 CCmdTarget。我使用 IDispatchTarget 作为我的通用基类。如果您需要您的设计在同一个处理机制中支持不同的基类型,那么我建议参考前面提到的其他文章之一。

它被称为句柄(handler),因为这是我当时使用的语义。我曾接触过一些 C#,在 2001 年参加了 DevelopMentor 的 Guerrilla.NET 课程,但实际上我一直使用 C++,所以 .NET 只是有足够的机会渗透到我的潜意识中。我直到最近回到商业领域才真正开始考虑 .NET 或 C# 以及委托/事件关系,并意识到我所模仿的东西。回到过去清理这篇文章的代码让我感到有些痛苦,因为现在我被 C# 和 .NET 框架宠坏了。我非常欣赏这两个语言和框架所付出的努力和最终的设计。话虽如此,我仍然是 C++ 的爱好者。我是在 C++ 上成长的,而且当您需要性能提升时,在速度和语义方面,它是无与伦比的。

使用代码

我构建此代码是为了能够直接将成员函数指针赋值给一个最多接受七个参数的 functor,并且希望支持返回值。因此,我采取了默认模板参数的方法,并使用一个 enum 来指定使用的参数数量。这是一种有些蛮力的方法,但它消除了为每个参数数量不同的函数签名使用不同模板的需要。

赋值遵循标准的成员函数指针语法。使用时,只需用父对象的 this 指针初始化句柄,并将选定的成员函数直接赋给句柄。

用法将如下所示:

#include<tchar.h>
#include <iostream>

using namespace std;

// Forward declare our class for its type
// to use in the template declaration.
class MHTester;

// Declare our typed handler using
// the parent's type, the number of params,
// whether we're using a return,
// the return type, and the parameter list.
// Use the default type TDEF_DATA for
// a return value if specifying NO for a return.
typedef TMappedHandler<MHTester, e1Params, YES, 
                       bool, int> TestHandler;

class MHTester 
{ 
   // Within our test class we'll declare our handler.
   TestHandler m_handler; 
public: 
   // We need to intialize our handler with this.
   MHTester() : m_handler(this) 
   { 
   // Assign our member function pointer.
      m_handler = &MHTester::TestMethod; 
   } 
   // Supply access to the handler.
   TestHandler &GetHandler() 
   { 
      return m_handler; 
   } 
   // Our test method that's mapped to the handler.
   bool TestMethod(int testValue) 
   { 
      cout << "\t" << testValue 
           << "\tis the number being tested!\n\n"; 
      return true; 
   } 
}; 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
   // Declare our test object.
   MHTester tester; 

   // Grab our handler, from here it can go to a queue,
   // allocate on the heap if its going
   // to a threaded event handler.
   // The object creating the handler
   // determines its allocation.
   // Also, any object that is mapped
   // to the handler needs to be on the heap.
   TestHandler handler = tester.GetHandler(); 

   // Call our handler using the call operator
   // like a good functor should.
   handler(5); 
   handler(6); 
   return 0; 
}

正如我们所见,用法足够简单。我们进行赋值和调用。只要我们注意多线程条件,就不会有什么问题。那么,它内部是怎么工作的呢?我们来看看。

// Define our default and hidden types
namespace def 
{ 
   class Default { public: Default() { } }; 
   class emptyVoidReturnSigAmibiguityFix { }; 
}; 

//Our default data type, small and flexible 
typedef def::Default& TDEF_DATA; 

//our hidden parameter types defined to fix the abiguity call 
//of a signature differing only by return type, 
//this is for the call operators 
typedef def::emptyVoidReturnSigAmibiguityFix* HiddenParam; 
#define HiddenParamVal (def::emptyVoidReturnSigAmibiguityFix*)0 

//some enumerations used in TMappedHandler but publicly available 
typedef enum { e0Params = 0, e1Params, e2Params, e3Params, e4Params, 
               e5Params, e6Params, e7Params } eNumberOfParams; 
typedef enum { NO = 0, YES = 1 } eReturn; 

#define SIGNATURE_ALGO(P, R)\ 
   ((P == e0Params && R == NO) ? 0 : (1+((1+R)*P)))

首先,我们有一些默认值需要设置。其中大多数是在编译过程中发现的。需要默认值,以免与用户定义的类型冲突。另一件需要处理的、以免冲突的事情是处理因两个仅返回类型不同的调用运算符而产生的歧义。由于我是在 VC 6 上开发的,所以我没有使用模板的任何增强功能。因此,这是采取的方法。然后,定义了一些枚举来确定参数的数量和返回值。参数的数量用作宏 SIGNATURE_ALGO 使用的函数指针的提示。它也用于增强版本,该版本旨在接收数据流,并使用内部解析器来解析参数,然后调用相应的句柄。所有这些都打算用于框架内的远程过程调用和脚本。

当映射一个返回 void 的函数时,就会出现问题。我倾向于从类型定义的模板定义继承一个类,然后通过提供自己的调用运算符来传入隐藏的参数,从而隐藏对隐藏参数的依赖。这确实不是最优雅的,并且可能会被重构。

class Handler : TMappedHandler<MappedParent>
{
   Handler() : HandlerBase(0) {} 
public:
   Handler(const Handler &rhs) : HandlerBase(rhs) 
   {  
   }  
   Handler &operator=(const Handler &rhs) 
   { 
       if(&rhs == this)  
           return *this; 
        HandlerBase::operator=(rhs); 
        return *this; 
   }
   Handler(MappedParent *pParent) : HandlerBase(pParent) 
   {  
       HandlerBase::operator=(&MappedParent::MethodName); 
   }  
   void operator()()  
   {  
       HandlerBase::operator()(HiddenParamVal);  
   }  
};

这允许我们隐藏细节,同时也允许调用者不必了解 void 返回方法所需的隐藏参数。

现在,进入实现的精华部分,模板本身

template<typename tMapParent = def::Default, 
      eNumberOfParams ePARAMS = e0Params, eReturn eRET = NO, 
      typename tResult = TDEF_DATA, typename tP1 = TDEF_DATA, 
      typename tP2 = TDEF_DATA, typename tP3 = TDEF_DATA, 
      typename tP4 = TDEF_DATA, typename tP5 = TDEF_DATA, 
      typename tP6 = TDEF_DATA, typename tP7 = TDEF_DATA> 
class TMappedHandler 
{
   TMappedHandler() : m_pThis(0) { } 

   tMapParent *m_pThis; 
protected:
   typedef TMappedHandler<tMapParent, ePARAMS, eRET, tResult, tP1, 
      tP2, tP3, tP4, tP5, tP6, tP7> HandlerBase; 
public:   
  
   TMappedHandler(const HandlerBase &rhs) { *this = rhs; } 
   TMappedHandler(tMapParent *pThis) : m_pThis(pThis) { } 
   typedef void(tMapParent::*FX_pDef)(); 
   typedef tResult(tMapParent::*FX_p0PWRet)(); 
   typedef void(tMapParent::*FX_p1PWORet)(tP1); 
   typedef tResult(tMapParent::*FX_p1PWRet)(tP1); 
   typedef void(tMapParent::*FX_p2PWORet)(tP1, tP2); 
   typedef tResult(tMapParent::*FX_p2PWRet)(tP1, tP2); 
   typedef void(tMapParent::*FX_p3PWORet)(tP1, tP2, tP3); 
   typedef tResult(tMapParent::*FX_p3PWRet)(tP1, tP2, tP3); 
   typedef void(tMapParent::*FX_p4PWORet)(tP1, tP2, tP3, tP4); 
   typedef tResult(tMapParent::*FX_p4PWRet)(tP1, tP2, tP3, tP4); 
   typedef void(tMapParent::*FX_p5PWORet)(tP1, tP2, tP3, tP4, tP5); 
   typedef tResult(tMapParent::*FX_p5PWRet)(tP1, tP2, tP3, tP4, tP5); 
   typedef void(tMapParent::*FX_p6PWORet)(tP1, tP2, tP3, tP4, tP5, tP6); 
   typedef tResult(tMapParent::*FX_p6PWRet)(tP1, tP2, tP3, tP4, tP5, tP6); 
   typedef void(tMapParent::*FX_p7PWORet)(tP1, tP2, tP3, tP4, tP5, tP6, tP7); 
   typedef tResult(tMapParent::*FX_p7PWRet)(tP1, tP2, tP3, tP4, tP5, tP6, tP7); 
protected:
   union MMF 
   { 
      FX_pDef     pfn; 
      FX_p0PWRet  pfn0; 
      FX_p1PWORet pfnV1; 
      FX_p1PWRet  pfn1; 
      FX_p2PWORet pfnV2; 
      FX_p2PWRet  pfn2; 
      FX_p3PWORet pfnV3; 
      FX_p3PWRet  pfn3; 
      FX_p4PWORet pfnV4; 
      FX_p4PWRet  pfn4; 
      FX_p5PWORet pfnV5; 
      FX_p5PWRet  pfn5; 
      FX_p6PWORet pfnV6; 
      FX_p6PWRet  pfn6; 
      FX_p7PWORet pfnV7; 
      FX_p7PWRet  pfn7; 
   }; 
   FX_pDef m_pfn; 
public:

   //The basic assignment operator.
   HandlerBase& operator=(const HandlerBase &rhs) 
   { 
      if(&rhs == this) 
         return *this; 
      m_pThis = rhs.m_pThis; 
      m_pfn = rhs.m_pfn; 
      return *this; 
   }

   //Assignment operators for all of our function types.
   FX_pDef operator=(FX_pDef pfn) 
   { 
      m_pfn = pfn; 
      return pfn; 
   } 
   FX_p0PWRet operator=(FX_p0PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p1PWORet operator=(FX_p1PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p1PWRet operator=(FX_p1PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p2PWORet operator=(FX_p2PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p2PWRet operator=(FX_p2PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p3PWORet operator=(FX_p3PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p3PWRet operator=(FX_p3PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p4PWORet operator=(FX_p4PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p4PWRet operator=(FX_p4PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p5PWORet operator=(FX_p5PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p5PWRet operator=(FX_p5PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p6PWORet operator=(FX_p6PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p6PWRet operator=(FX_p6PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p7PWORet operator=(FX_p7PWORet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 
   FX_p7PWRet operator=(FX_p7PWRet pfn) 
   { 
      m_pfn = (FX_pDef)pfn; 
      return pfn; 
   } 

   //HiddenParam is a placeholder allowing the mapping of methods 
   //differing only by return type; meant to be wrapped by a deriving 
   //handler to obscure its details. 
   typedef HiddenParam HP;
  
   //These macros make the calls a little 
   //simpler for the call operators.

   #define MMF_CALL(N) MMFmmf;mmf.pfn=m_pfn;(m_pThis->*mmf.pfnV##N) 
   #define return_MMF_CALL(N) MMFmmf;mmf.pfn=m_pfn;
                              return(m_pThis->*mmf.pfn##N) 

   //Call operators 
   void operator()(HP) 
   { 
      (m_pThis->*m_pfn)(); 
   } 
   tResult operator()() 
   { 
      return_MMF_CALL(0)(); 
   } 
   void operator()(tP1 p1,HP) 
   { 
   MMF_CALL(1)(p1); 
   } 
   tResult operator()(tP1 p1) 
   { 
      return_MMF_CALL(1)(p1); 
   } 
   void operator()(tP1 p1,tP2 p2,HP) 
   { 
      MMF_CALL(2)(p1,p2); 
   } 
   tResult operator()(tP1 p1,tP2 p2) 
   { 
      return_MMF_CALL(2)(p1,p2); 
   } 
   void operator()(tP1 p1,tP2 p2,tP3 p3,HP) 
   { 
      MMF_CALL(3)(p1,p2,p3); 
   } 
   tResult operator()(tP1 p1,tP2 p2,tP3 p3) 
   { 
      return_MMF_CALL(3)(p1,p2,p3); 
   } 
   void operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,HP) 
   { 
      MMF_CALL(4)(p1,p2,p3,p4); 
   } 
   tResult operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4) 
   { 
      return_MMF_CALL(4)(p1,p2,p3,p4); 
   } 
   void operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,tP5 p5,HP) 
   { 
      MMF_CALL(5)(p1,p2,p3,p4,p5); 
   } 
   tResult operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,tP5 p5) 
   { 
      return_MMF_CALL(5)(p1,p2,p3,p4,p5); 
   } 
   void operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,tP5 p5,tP6 p6,HP) 
   { 
      MMF_CALL(6)(p1,p2,p3,p4,p5,p6); 
   } 
   tResult operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,tP5 p5,tP6 p6) 
   { 
      return_MMF_CALL(6)(p1,p2,p3,p4,p5,p6); 
   } 
   void operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,tP5 p5,tP6 p6,tP7 p7,HP) 
   { 
      MMF_CALL(7)(p1,p2,p3,p4,p5,p6,p7); 
   } 
   tResult operator()(tP1 p1,tP2 p2,tP3 p3,tP4 p4,tP5 p5,tP6 p6,tP7 p7) 
   { 
      return_MMF_CALL(7)(p1,p2,p3,p4,p5,p6,p7); 
   } 
};

就这样。不算太复杂,但它利用了一个 hack,即利用了模拟 MFC 的联合方法,但试图减少对底层类型的破坏。我不需要比较句柄,所以没有实现那些运算符。但是,只要一个通用的基接口符合您的设计,就可以从这个初始句柄派生出一个方便的事件系统。

我只在 MSVC 6、MSVC 2003 和 GCC 4.1.1 上对其进行了测试,所以它可能不适用于所有编译器。同样,我的需求只是为了构建 Linux 和 Windows 应用程序。

历史

  • 2006/8/9:首次发布。
© . All rights reserved.