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

C++ 运行时多态,无需虚函数。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (35投票s)

2013年6月7日

CPOL

13分钟阅读

viewsIcon

96050

downloadIcon

556

在没有虚函数的情况下实现动态多态,并跨进程边界共享对象。

简介  

共享 C++ 对象跨越进程边界需要特别注意。其中一个问题是,虚函数只能由创建对象的进程安全调用。本文介绍了一种模式,该模式可用于为 C++ 类提供类似虚函数的功能,而无需使用虚函数,从而克服这一限制。使用此模式,共享对象的异构容器可以被任何共享对象的进程迭代,以调用它们动态绑定的函数。

背景  

共享 C++ 对象

由于在 [MSDN: 如何让我的 DLL 与应用程序或其他 DLL 共享数据?] 中讨论的限制,在进程之间共享 C++ 对象通常不被推荐。尽管如此,当克服这些限制时,跨进程边界共享对象是可能的。那么,在进程之间共享 C++ 对象有哪些注意事项呢?

  1. 使用动态绑定时,当在对象上调用虚成员函数时,编译器无法解析函数调用,因为它不知道应该调用哪个函数。为了解析虚函数调用,编译器为定义了虚函数的每个类创建一个虚函数表 (vtbl)。vtbl 包含类定义的虚函数的偏移量。当进程在运行时创建一个类的对象时,该对象会被分配一个指向该类 vtbl 的指针。由于此指针位于创建对象的进程的数据段中,即使对象是在共享内存段中创建的,vtbl 指针也仅对创建对象的进程有效。这意味着尝试调用共享对象的虚函数的其他进程将会崩溃(除非不同进程的虚函数表碰巧位于相同的虚拟地址,这可能无法由操作系统保证)。这是考虑跨进程共享对象动态绑定替代方案的主要原因。
  1. 在共享内存中创建的 C++ 对象的指针在不同进程中仅在它们都将共享内存段映射到相同的虚拟地址时才有效,但这可能无法由操作系统保证(操作系统可能提供建议虚拟地址和段大小的机制,并且只有在虚拟地址不被使用时才会将进程映射到该地址)。解决方案是使用指向映射到共享内存的虚拟地址的偏移量,并在运行时通过将偏移量加到基址虚拟地址来创建指向对象的指针。类似地,而不是在 C++ 类中拥有成员指针,我们需要拥有成员偏移量(到映射到共享内存的基址虚拟地址)。
  1. 编译器会将类的静态数据成员分配到进程的默认数据段,因此不同进程将拥有这些成员的不同副本。这可能是类的预期设计,但如果需要静态数据成员的单个副本,则应将数据成员替换为指向映射到共享内存的基址虚拟地址的偏移量。
  1. 不同的进程同时调用共享对象的成员函数时,它们可能会相互干扰,导致数据损坏。必须使用 IPC(进程间通信)机制来提供互斥。

奇偶递归模板模式 (Curiously Recurring Template Pattern)

"奇偶递归模板模式" (CRTP) 是动态绑定的常用替代方法。CRTP 用于实现静态多态(也称为模拟动态绑定)[Wikipedia]。静态多态实现了与使用虚函数类似的效果,允许在编译时而不是运行时选择派生类中的重载函数。使用 CRTP,一个“派生”类继承一个“Base<Derived>”模板类,其中 Base 类通过类型转换对象并调用其接口成员函数来实现 Derived 类的接口函数。为了正确删除派生类实例,Base 类首先继承自一个通用的 Deletor 类,该类定义了一个虚析构函数。虚析构函数通过基类指针提供派生对象的删除。

class Deletor {
    public:  virtual ~Deletor() {}
};
 
template<typename T> class Base : public Deletor {
    public: 
 
        int  Run() { return static_cast<T*>(this)->DoIt(); }
};
 
class Derived1 : public Base<Derived1> {
    ...
    public:
        int  DoIt() { /* the actual implementation for Derived1 */  }
};
 
class Derived2 : public Base<Derived2> {
    ...
    public:
        int  DoIt() { /* the actual implementation for Derived2 */  }
};
 
int main() {
    Derived1 Obj1;
    Derived2 Obj2;
 
    Obj1.Run(); /* runs the actual DoIt() implementation */
    Obj2.Run(); /* runs the actual DoIt() implementation */
};

如果不使用像这里那样通用的 Base 类(如 Deletor),则派生类不能作为异构存储,因为每个 CRTP Base 类都是一个唯一的类型。Base<Derived1>Base<Derived2> 是不相关的类,因此,即使这些对象现在可以作为异构对象存储在 Base Deletor* 对象的容器中,也不能迭代它们以提供运行时多态性并泛化地调用对象的成员函数(例如,上面示例中的 DoIt())。CRTP 非常适合客户端需要创建单一类型派生类的应用程序。

模拟 C++ 接口模板模式

我采取了一种不同的方法,即模拟 C++ 接口模板模式(我将简称为 SITP)。这里介绍的 SITP 模式依赖于静态模板成员函数,而不是模板类。此模式要求创建一个 Base 类,该类定义静态模板成员函数,这些函数将访问派生类的接口成员函数。目标是能够将派生类的对象添加到容器中,然后在以后通过迭代容器并调用每个对象的接口成员函数来泛化地访问它们,而无需了解有关对象类型的任何详细信息;我们只需要知道该对象继承自一个通用 Base 类,该类定义了一组成员函数的接口,这些函数遵循特定的函数调用模式。

假设我们需要一个具有动态绑定方法 "int siRun( int& )" 的类层次结构。然后,我们创建一个 RunnableInterface Base 类,它具有一个静态模板成员函数 Run_T,以及一个虚析构函数,以便通过 Base 类指针提供正确的派生对象销毁。

Run_T(..) 具有与必需的 siRun(..) 成员函数相同的函数协议,但前面有一个额外的参数,即操作静态模板函数的对象指针。然后,我们定义一个指向成员函数的指针的成员变量,一个将其初始化为 null 的构造函数,一个模板成员函数 (Init) 来将成员变量设置为指向静态成员函数(&Run_T<T>)的正确实现,以及一个公共成员函数实现 (siRun),它具有与静态模板成员函数 (Run_T) 相同的参数和返回类型,但排除了第一个参数(它是对象指针)以便通过成员变量间接调用静态模板函数。

class RunnableInterface {
   private:
 
      typedef int (*PFN_RUN_T)(void* const, int&);
 
      PFN_RUN_T    m_pfnRun_T;
 
     template<typename T> static 
                             int Run_T( void* const pObj, int& k ) {
                                    return static_cast<T*>(pObj)->siRun( k ); }
   protected:
      template<typename T>  void Init() { m_pfnRun_T = (PFN_RUN_T) &Run_T<T>; }
 
   public:
                             RunnableInterface() : m_pfnRun_T( 0 ) {}
      virtual               ~RunnableInterface() {}
 
      int                    siRun( int& k ) {
                               assert( m_pfnRun_T ); // Make sure Init() was called.
                               return (*m_pfnRun_T)( this, k ); }
};
请注意,指针成员变量与模板参数类型 T 没有任何关系。使用通用的 "void* const pObj" 指针来提供一个独立的 RunnableInterface 类实例,该实例与派生类的类型无关。此时,您可能会说,这对于共享对象没有帮助,因为我们现在有一个指针成员,它可能在进程边界处无效,但请暂时忍受,我稍后会解决这个问题。另外,请注意,对于此类共享对象,类仍然可以具有虚方法,只要这些方法仅由创建对象的进程调用;RunnableInterface 类有一个虚析构函数,它通过 Base 类指针提供派生对象的销毁;这与共享对象一起工作,只要创建对象的进程也负责其销毁。

但是,如果派生类无意中抑制了某个接口成员函数的定义(例如 siRun()),那么模板函数中相应的 static_cast 在编译时不会失败,并且当尝试调用未定义的接口成员函数时,应用程序最终会在运行时崩溃。幸运的是,SFINAE(Substitution Failure Is Not An Error)技术可用于在运行时检查接口成员函数是否存在 [Wikipedia];"CreateMemberFunctionChecker" 宏实现了一个结构,该结构在编译时为指定类解析一个静态 "value",以便稍后在运行时检查接口成员函数是否为该类定义。然后,可以在 Init() 中调用相应的 "CheckMemberFunction" 宏来断言在运行时接口成员函数的存在。两个宏中的 FNNAME 参数必须指定成员函数名(例如,此处讨论的示例中的 "siRun");"CheckMemberFunction" 中的第二个参数必须指定使用 T 类型名的成员函数原型(例如,此处讨论的 siRun 示例为 int (T::*)(int&))。

// Using the SFINAE (Substitution Failure Is Not An Error) technique,
// the following macro creates a template class with typename T and a
// static boolean member "value" that is set to true if the specified
// member function exists in the T class.
// This macro was created based on information that was retrieved from
// the following URLs:
// https://groups.google.com/forum/?fromgroups#!topic/comp.lang.c++/DAq3H8Ph_1k
// http://objectmix.com/c/779528-call-member-function-only-if-exists-vc-9-a.html
 
#define CreateMemberFunctionChecker( FNNAME )                            \
  template<typename T> struct has_member_##FNNAME;                       \
                                                                         \
  template<typename R, typename C> class has_member_##FNNAME<R C::*> {   \
     private:                                                            \
        template<R C::*> struct helper;                                  \
        template<typename T> static char check(helper<&T::FNNAME>*);     \
        template<typename T> static char (& check(...))[2];              \
     public:                                                             \
        static const bool value = (sizeof(check<C>(0)) == sizeof(char)); \
  }
 
// This corresponding macro is used to check the existence of the
// interface function in the derived class.
#define CheckMemberFunction( FNNAME, FNPROTOTYPE ) {                     \
              assert( has_member_##FNNAME<FNPROTOTYPE>::value ); }

因此,使用这些宏,这是完整的 RunnableInterface Base 类。

class RunnableInterface {
   private:
      CreateMemberFunctionChecker( siRun );
 
      typedef int (*PFN_RUN_T)(void* const, int&);
 
      PFN_RUN_T     m_pfnRun_T;
 
      template<typename T>
         static int              Run_T( void* const pObj, int& k ) {
                                     return static_cast<T*>(pObj)->siRun( k ); }
   protected:
      template<typename T> void  Init() {
                                    CheckMemberFunction( siRun, int (T::*)(int&) );
                                    m_pfnRun_T = (PFN_RUN_T) &Run_T<T>; }
   public:
                                 RunnableInterface() : m_pfnRun_T( 0 ) {}
      virtual                   ~RunnableInterface() {}
      int                        siRun( int& k ) {
                                    // Make sure Init() was called.
                                    assert( m_pfnRun_T );
                                    return (*m_pfnRun_T)( this, k ); }
}; 

此类现在可以成为重载 int siRun( int& k ) 函数的类的 Base 类,该函数实现特定于派生类的行为,而我们在派生类构造函数中所要做的就是调用 "Init<Derived>();" 来将派生类的 int Derived::siRun( int& k ) 成员连接到 Base 类静态模板函数 (Run_T),如下所示。

class Test : public RunnableInterface {
  friend class RunnableInterface;
  private:
    int      siRun( int& k ) { k = m_value * 2; return 0; }
  protected:
    int      m_value;
  public:
             Test( int value ) : m_value( value ) {
                                   RunnableInterface::Init<Test>(); }
};
 
class AdjustmentTest : public Test {
  friend class RunnableInterface;
  private:
    int      siRun( int& k ) { k = m_value * 3; return 0; }
  public:
             AdjustmentTest( int value ) : Test( value ) {
                                   RunnableInterface::Init<AdjustmentTest>(); }
};

需要 "friend RunnableInterface;" 以允许访问 RunnableInterface 类中定义的模板函数,同时保持接口成员函数的范围为私有或保护。

现在我们可以迭代 RunnableInterface* 对象的异构容器来访问重载函数(这不能通过 CRTP 实现),如下所示:

int main()
{  
 
   RunnableInterface* const pObj1 = new Test( 1 );
   RunnableInterface* const pObj2 = new AdjustmentTest( 4 );
 
   std::list<RunnableInterface*> list1;
   list1.insert( list1.end(), pObj1 );
   list1.insert( list1.end(), pObj2 );
 
   std::list< RunnableInterface *>::iterator i;
   for ( i = list1.begin(); i != list1.end(); i++ ) {
      RunnableInterface* const p = *i;
 
      int k;
      const int j = p->siRun( k );
 
      std::cout << "RUN: " << j << ":" << k << std::endl << std::endl;
 
      delete p;
   }
 
   return 0;
}

但等等!!!RunnableInterface Base 类有一个函数指针成员变量。如果我们运行在单个进程中,并且我们想将此模式用于共享对象跨进程边界以外的用途,那么这将没有问题。如果操作系统为同一程序的多个实例分配相同的代码虚拟地址,那么即使是共享内存中的对象,它也可能在某些操作系统上工作。但要保证它在进程边界之间工作,我们确实应该使用到模块加载地址的偏移量,而不是函数指针。在 Windows 中,GetModuleHandle() 返回的模块句柄与模块的加载地址相同 [MODULEINFO 结构]。然后可以在运行时计算函数指针,以生成对调用进程有效的地址。下面显示了适用于 Windows 的可共享 RunnableInterface Base 类。

class RunnableInterface {
   private:
      template <typename T> static int Run_T( void* const pObj, int& k ) {
                                          static_cast<T*>(pObj)->siRun( k ); }
 
      typedef int (*PFN_RUN_T)(void* const, int&);
 
      CreateMemberFunctionChecker( siRun );
 
      // Offset to static template member functions.
      unsigned long m_ulRun_T_Offset;
 
   protected:
      template <typename T>
      void     Init() {
                  CheckMemberFunction( siRun, int (T::*)(int&) );
 
                  // Assign the derived class' member functions offsets to the
                  // TestInterface base class member variables.
                  // NOTE: The load address of a module is the same as the
                  //       HMODULE value.
                  //       Ref: http://msdn.microsoft.com/en-us/library/windows/
                  //                              desktop/ms684229(v=vs.85).aspx
                  char* pBaseOffset = (char*) GetModuleHandle( NULL );
                  m_ulRun_T_Offset = (unsigned long) ((PFN_RUN_T) &Run_T<T>) -
                                                   (unsigned long) pBaseOffset;
               }
   public:
      int      siRun( int& k ) {
                   assert( m_ulRun_T_Offset ); // Make sure Init() was called.
                   char* const pBaseOffset = (char*) GetModuleHandle(NULL);
                   PFN_RUN_T pfnRun_T = (PFN_RUN_T)
                                             (pBaseOffset + m_ulRun_T_Offset);
                   return (*pfnRun_T)( this, k ); }
 
               RunnableInterface() : // Initialize member variables.
                           m_ulRun_T_Offset( 0 ) {}
      virtual ~RunnableInterface() {}
};

Using the Code

列表 1 中的代码示例演示了启动该机制并共享 C++ 对象跨进程边界的简单性。它是使用 Qt Creator 5.0.2 - MinGW 4.7 32 位以及 Visual Studio 2010 构建的,并在 Windows XP 和 Windows 7 64 位下进行了测试。

使用 MinGW 构建时,为了运行生成的 test.exe,需要以下 DLL:libgcc_s_sjlj-1.dll、libstdc++-6.dll 和 libwinpthread-1.dll。

列表 1 中显示的 testiface.h 文件呈现了 TestInterface 类,该类定义了以下成员函数的接口:

  int   siRun();
  void siReset( int& k );
  void siSayHello();

testiface.h – 列表 1
#ifndef TESTIFACE_H
#define TESTIFACE_H
 
#include <windows.h> // for GetModuleHandle()
 
// Using the SFINAE (Substitution Failure Is Not An Error) technique,
// the following macro creates a template class with typename T and a
// static boolean member "value" that is set to true if the specified
// member function exists in the T class.
// This macro was created based on information that was retrieved from
// the following URLs:
// https://groups.google.com/forum/?fromgroups#!topic/comp.lang.c++/DAq3H8Ph_1k
// http://objectmix.com/c/779528-call-member-function-only-if-exists-vc-9-a.html
 
#define CreateMemberFunctionChecker( FNNAME )                           \
template<typename T> struct has_member_##FNNAME;                        \
                                                                        \
template<typename R, typename C> class has_member_##FNNAME<R C::*> {    \
   private:                                                             \
      template<R C::*> struct helper;                                   \
      template<typename T> static char check(helper<&T::FNNAME>*);      \
      template<typename T> static char (& check(...))[2];               \
   public:                                                              \
      static const bool value = (sizeof(check<C>(0)) == sizeof(char));  \
}
 
// This macro is used to check the existence of the interface function
// in the derived class.
#define CheckMemberFunction( FNNAME, FNPROTOTYPE ) { \
              assert( has_member_##FNNAME<FNPROTOTYPE>::value ); }
 
typedef int  (*PFN_RUN_T)(void* const);
typedef void (*PFN_RESET_T)(void* const, int&);
typedef void (*PFN_SAYHELLO_T)(void* const);
 
#ifndef SINGLE_PROCESS
class TestInterface {
   private:
      // Implement template functions.
      template <typename T> static int  Run_T( void* const pObj ) {
                                          return static_cast<T*>(pObj)->siRun(); }
      template <typename T> static void Reset_T( void* const pObj, int& k ) {
                                          static_cast<T*>(pObj)->siReset( k ); }
      template <typename T> static void SayHello_T( void* const pObj ) {
                                          static_cast<T*>(pObj)->siSayHello(); }
 
      // These macros expand into template classes which are used to check
      // the existence of the specified interface function in derived classes.
      CreateMemberFunctionChecker( siRun );
      CreateMemberFunctionChecker( siReset );
      CreateMemberFunctionChecker( siSayHello );
 
      // Offsets to static template member functions.
      unsigned long m_ulRun_T_Offset,
                    m_ulReset_T_Offset,
                    m_ulSayHello_T_Offset;
 
   protected:
      template <typename T>
      void     Init() {
                  // Make sure the interface member functions in derived
                  // classes have been defined with the correct parameters
                  // and return type.
                  CheckMemberFunction( siRun, int (T::*)() );
                  CheckMemberFunction( siReset, void (T::*)(int&) );
                  CheckMemberFunction( siSayHello, void (T::*)() );
 
                  // Assign the derived class' member functions offsets to the
                  // TestInterface base class member variables.
                  // NOTE: The load address of a module is the same as the
                  //       HMODULE value.
                  //       Ref: http://msdn.microsoft.com/en-us/library/windows/
                  //                              desktop/ms684229(v=vs.85).aspx
                  char* pBaseOffset = (char*) GetModuleHandle( NULL );
                  m_ulRun_T_Offset = (unsigned long) ((PFN_RUN_T) &Run_T<T>) -
                                                   (unsigned long) pBaseOffset;
                  m_ulReset_T_Offset = (unsigned long) ((PFN_RESET_T) &Reset_T<T>) -
                                                   (unsigned long) pBaseOffset;
                  m_ulSayHello_T_Offset= (unsigned long) ((PFN_SAYHELLO_T) &SayHello_T<T>) -
                                                   (unsigned long) pBaseOffset;
               }
   public:
      int      siRun() {
                   assert( m_ulRun_T_Offset ); // Make sure Init() was called.
                   char* pBaseOffset = (char*) GetModuleHandle(NULL);
                   PFN_RUN_T pfnRun_T = (PFN_RUN_T)
                                               (pBaseOffset + m_ulRun_T_Offset);
                   return (*pfnRun_T)( this ); }
      void     siReset( int& k ) {
                   assert( m_ulReset_T_Offset ); // Make sure Init() was called.
                   char* pBaseOffset = (char*) GetModuleHandle(NULL);
                   PFN_RESET_T pfnReset_T = (PFN_RESET_T)
                                             (pBaseOffset + m_ulReset_T_Offset);
                   (*pfnReset_T)( this, k ); }
      void     siSayHello() {
                   assert( m_ulSayHello_T_Offset ); // Make sure Init() was called.
                   char* pBaseOffset = (char*) GetModuleHandle(NULL);
                   PFN_SAYHELLO_T pfnSayHello_T = (PFN_SAYHELLO_T)
                                          (pBaseOffset + m_ulSayHello_T_Offset);
                   (*pfnSayHello_T)( this ); }
 
               TestInterface() : // Initialize member variables.
                           m_ulRun_T_Offset( 0 ),
                         m_ulReset_T_Offset( 0 ),
                      m_ulSayHello_T_Offset( 0 ) {}
      virtual ~TestInterface()  {}
};
#else
class TestInterface {
   private:
      // Implement template functions.
      template <typename T> static int  Run_T( void* const pObj ) {
                                          return static_cast<T*>(pObj)->siRun(); }
      template <typename T> static void Reset_T( void* const pObj, int& k ) {
                                          static_cast<T*>(pObj)->siReset( k ); }
      template <typename T> static void SayHello_T( void* const pObj ) {
                                          static_cast<T*>(pObj)->siSayHello(); }
 
      // Pointers to static template member functions.
      PFN_RUN_T      m_pfnRun_T;       // Pointer to Run_T()
      PFN_RESET_T    m_pfnReset_T;     // Pointer to Reset_T()
      PFN_SAYHELLO_T m_pfnSayHello_T;  // Pointer to SayHello_T()
 
      // These macros expand into template classes which are used to check
      // the existence of the specified interface function in derived classes.
      CreateMemberFunctionChecker( siRun );
      CreateMemberFunctionChecker( siReset );
      CreateMemberFunctionChecker( siSayHello );
 
   protected:
      template <typename T>
      void     Init() {
                  // Make sure the interface member functions in derived
                  // classes have been defined with the correct parameters
                  // and return type.
                  CheckMemberFunction( siRun, int (T::*)() );
                  CheckMemberFunction( siReset, void (T::*)(int&) );
                  CheckMemberFunction( siSayHello, void (T::*)() );
 
                  // Assign the derived class' member functions to the
                  // TestInterface base class member variables.
                  m_pfnRun_T      = (PFN_RUN_T) &Run_T<T>;
                  m_pfnReset_T    = (PFN_RESET_T) &Reset_T<T>;
                  m_pfnSayHello_T = (PFN_SAYHELLO_T) &SayHello_T<T>; }
   public:
      int      siRun() {
                   assert( m_pfnRun_T ); // Make sure Init() was called.
                   return (*m_pfnRun_T)( this ); }
      void     siReset( int& k ) {
                   assert( m_pfnReset_T ); // Make sure Init() was called.
                   (*m_pfnReset_T)( this, k ); }
      void     siSayHello() {
                   assert( m_pfnSayHello_T ); // Make sure Init() was called.
                   (*m_pfnSayHello_T)( this ); }
 
               TestInterface() : // Initialize member variables.
                           m_pfnRun_T( 0 ),
                         m_pfnReset_T( 0 ),
                      m_pfnSayHello_T( 0 ) {}
      virtual ~TestInterface() {}
};
#endif // SINGLE_PROCESS
 
#endif // TESTIFACE_H

列表 2 中显示的 testclasses.h 文件演示了三个派生类的创建:Base(继承自 TestInterface)、DerivedOnce(继承自 Base)和 DerivedTwice(继承自 DerivedOnce)。对于这些类中的每一个,其构造函数都调用 TestInterface::Init<T>(),其中 T 被替换为类的名称。这是派生类实现 TestInterface 机制所需的全部。在这些类中的每一个中,TestInterface 都被声明为 friend 类,只是为了允许派生类的接口函数 siRunsiResetsiSayHello 对其他人保持私有(或保护)范围。

testclasses.h – 列表 2
#ifndef TESTCLASSES_H
#define TESTCLASSES_H
 
#ifndef TESTIFACE_H
   #include "testiface.h"
#endif
 
class Base : public TestInterface {
  // The friend TestInterface is needed to give access to the template
  // functions that are defined within the TestInterface class while
  // preventing access to the interface member functions defined here
  // by making them private or protected.
  friend class TestInterface;
 
  private:
    int      siRun() { return m_value; }
    void     siReset( int& k ) { k = m_value * 10; }
    void     siSayHello() { std::cout << "Hello from Base" << std::endl; }
 
  protected:
    int      m_value;
 
  public:
             Base( int value = 1 ) : m_value( value ) {
                                TestInterface::Init<Base>(); }
};
 
class DerivedOnce : public Base {
  friend class TestInterface;
 
  private:
    int      siRun() { return m_value; }
    void     siReset( int& k ) { k = m_value * 100; }
    void     siSayHello() {
                   std::cout << "Hello from DerivedOnce" << std::endl; }
  public:
             DerivedOnce() : Base() {
                                TestInterface::Init<DerivedOnce>();
                                ++m_value; }
};
 
class DerivedTwice : public DerivedOnce {
  friend class TestInterface;
 
  private:
      int    siRun() { return m_value; }
      void   siReset( int& k ) { k = m_value * 1000; }
      void   siSayHello() {
                     std::cout << "Hello from DerivedTwice" << std::endl; }
  public:
             DerivedTwice() : DerivedOnce() {
                                 TestInterface::Init<DerivedTwice>();
                                 ++m_value; }
};
 
#endif // TESTCLASSES_H

列表 3 中显示的 main.cpp 文件依赖于 Windows API 调用来演示

  1. "OWNER" 进程在共享内存中实例化 BaseDerivedOnceDerivedTwice 类,以及
  2. "CLIENT" 进程访问共享内存中的对象。

在这两种情况下,对象都被放入一个列表中,然后创建一个循环,将它们作为 TestInterface* 对象访问,以泛化地调用 siRunsiResetsiSayHello 接口成员函数。可以运行程序的多重实例来演示对象是如何成功地跨进程边界共享的。只需运行程序并保持运行状态而不按键;"OWNER"(共享内存和其中分配对象的创建者)将首先在控制台窗口中标识;然后运行程序的其他实例;"CLIENT" 将首先在控制台窗口中标识,您应该看到完全相同的输出(除了进程中共享内存的虚拟地址,这可能相同,也可能不同),但这里我们只是访问共享内存中的对象,并从不同的进程调用相同的代码。要正确终止程序,请先确认"CLIENT" 程序实例,然后确认"OWNER" 程序实例(它会调用对象的析构函数)。

尝试注释掉一个接口成员函数,例如 DerivedTwice::siSayHello(),然后重新构建应用程序;尝试运行时将导致断言失败并出现错误消息(由于在 TestInterface::Init<T>() 中调用了 CheckMemberFunction( siSayHello, void (T::*)() ))。

  Assertion failed!
  Expression: has_member_siSayHello<void (T::*)()>::value

尝试更改 DerivedTwicesiSayHello 的定义,使其具有一个参数(例如 "double d"),然后尝试重新构建应用程序;由于在 DerivedTwice 构造函数中调用 TestInterface::Init<DerivedTwice>(),当编译器在调用 DerivedTwice::siSayHello() 时找不到匹配的函数时,将生成编译时错误。

  In instantiation of ‘static void TestInterface::SayHello_T(void*) [with T = DerivedTwice]’;
  Required from ‘void TestInterface::Init [with T = DerivedTwice]

main.cpp – 列表 3
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
 
#include <list>
#include <iostream>
#include <conio.h>
#include <assert.h>
 
#include "testclasses.h"
 
int main()
{
   const SIZE_T BufSize = 1024;
   const TCHAR  szName[] = TEXT( "Local\\SharedMemBlockObject" );
 
   // For the process that creates the shared memory, there should be no
   // errors; but for the other processe(s), this call will be equivalent to
   // OpenFileMapping(..), and GetLastError() should return ERROR_ALREADY_EXISTS.
   const HANDLE hMapFile =
             CreateFileMapping(
                 INVALID_HANDLE_VALUE, // use paging file
                 NULL,                 // default security
                 PAGE_READWRITE,       // read/write access
                 0,                    // maximum object size (high-order DWORD)
                 BufSize,              // maximum object size (low-order DWORD)
                 szName );             // name of mapping object
   if ( hMapFile == NULL ) {
      std::cout << "Could not create file mapping object (" <<
                                         GetLastError() << ").\n" << std::endl;
      return 1;
   }
 
   const bool fFirstProcess = (GetLastError() != ERROR_ALREADY_EXISTS);
 
   // Map the file to this process' address space.
   const LPCTSTR pBuf =
           (LPTSTR) MapViewOfFile(
                                 hMapFile,            // handle to map object
                                 FILE_MAP_ALL_ACCESS, // read/write permission
                                 0,
                                 0,
                                 BufSize );
 
   if ( pBuf == NULL ) {
       std::cout << "Could not map view of file (" <<
                                          GetLastError() << ").\n" << std::endl;
      CloseHandle( hMapFile );
      return 1;
   }
 
   Base*         pObj1;
   DerivedOnce*  pObj2;
   DerivedTwice* pObj3;
 
   char*         pBuf1 = (char*) pBuf;
 
   if ( fFirstProcess ) {
      std::cout << "OWNER PROCESS: " << std::endl;
 
      // Create TestInterface objects in shared memory.
      // These objects have the means to call non-virtual interface member
      // functions defined in derived classes (named siRun(), siReset( int& )
      // and siSayHello()).
      pObj1 = new(pBuf1) Base;        // first available memory addr
      pBuf1 += sizeof( Base );        // skip to next available memory addr
      pObj2 = new(pBuf1) DerivedOnce;
      pBuf1 += sizeof( DerivedOnce ); // skip to next available memory addr
      pObj3 = new(pBuf1) DerivedTwice;
   }
   else {
      std::cout << "CLIENT PROCESS: " << std::endl;
 
      // Access objects that are in shared memory.
      pObj1 = (Base*) pBuf1;         // @addr where Base obj was created
      pBuf1 += sizeof( Base );
      pObj2 = (DerivedOnce*) pBuf1;  // @addr where DerivedOnce obj was created
      pBuf1 += sizeof( DerivedOnce );
      pObj3 = (DerivedTwice*) pBuf1; // @addr where DerivedTwice obj was created
   }
 
   char szHexBuf[12];
   sprintf( szHexBuf, "0x%lx", (unsigned long) pBuf );
   std::cout << "pBuf: " << szHexBuf << std::endl << std::endl;
 
   // Place TestInterface* objects in a list.
   std::list<TestInterface*> list1;
   list1.insert( list1.end(), pObj1 );
   list1.insert( list1.end(), pObj2 );
   list1.insert( list1.end(), pObj3 );
 
   // Let TestInterface objects greet, run and reset generically.
   std::list<TestInterface*>::iterator i;
   for ( i = list1.begin(); i != list1.end(); i++ ) {
      TestInterface* const p = *i;
 
      p->siSayHello();
 
      std::cout << "RUN:   " << p->siRun() << std::endl;
 
      int kk;
      p->siReset( kk );
      std::cout << "RESET: " << kk << std::endl << std::endl;
   }
 
   std::cout << "Press any key to end program" << std::endl;
   if ( fFirstProcess )
      std::cout << " and destroy objects in shared memory" << std::endl;
   std::cout << "..." << std::endl;
 
   while (!kbhit()) { Sleep( 100 ); }
 
   if ( fFirstProcess ) {
      // Objects are no longer needed, call destructors.
      for ( i = list1.begin(); i != list1.end(); i++ ) {
         TestInterface* const p = *i;
         // We need to call the destructor explicitly because
         // the new with placement operator was used.
         // We cannot call "delete p;"
         p->~TestInterface();
      }
   }
 
   UnmapViewOfFile( pBuf );
   CloseHandle( hMapFile );
 
   return 0;
} 

摘要

本文介绍的技术演示了一种可以在 C++ 类中使用的模式,该模式可以在消除虚函数的同时仍然提供类似虚函数的运行时动态绑定行为。该模式提供了迭代这些类的实例的异构容器并泛化地调用重载函数的能力。通过消除虚函数,可以共享 C++ 对象,同时保留跨进程边界提供动态绑定的能力。虽然 SITP 的语法可能不是理想的(即不像声明“virtual”函数那样简单),但它解决了语言未解决的一个问题,并且在功能上提供了与虚函数相同的运行时动态绑定。

参考文献

历史 

  • 2013 年 6 月 7 日 - 初始发布。
  • 2013 年 8 月 6 日 - 修正了代码以用 MSVC 编译。添加了 sitp.zip 下载链接。
  • 2014 年 1 月 5 日 - 修改了引言和摘要部分。在定义 "RunnableInterface 类" 后添加了关于使用虚析构函数和虚函数的一般说明。添加了历史记录部分。修正了拼写错误和格式。添加了对 MSDN 文章的引用。向现有部分添加了 "共享 C++ 对象" 和 "奇偶递归模板模式" 标题。
© . All rights reserved.