Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

C++ Runtime Polymorphism without Virtual Functions

4.90/5 (36 votes)
5 Jan 2014CPOL13 min read 95K   520  
Implementing dynamic polymorphism without virtual functions and sharing objects across process boundaries.

Introduction  

Sharing C++ objects across process boundaries requires special considerations. One of the issues is that virtual functions can be safely called only by the process that creates the object. This article presents a pattern that can be utilized to provide C++ classes with virtual-function-like functionality without using virtual functions in order to overcome that restriction.  Using this pattern a heterogeneous container of shared objects can be iterated by any process that is sharing the objects to invoke their dynamically bound functions.

Background  

Sharing C++ Objects

Sharing C++ objects between processes is usually not recommended due to restrictions that are discussed in [MSDN: How do I share data in my DLL with an application or with other DLLs?]. Nonetheless, sharing objects across process boundaries is possible when those restrictions are overcome. So what are the considerations for sharing C++ objects between processes?

  1. With dynamic binding, when a virtual member function is invoked on an object, the compiler cannot resolve the function call because it does not know which one should be called. In order to resolve virtual function calls, the compiler creates a virtual function table (vtbl) for each class that defines virtual functions. The vtbl contains offsets to the virtual functions that the class defines. When a process creates an object of the class at run time, the object is assigned a pointer to the vtbl of the class. Since this pointer is in the data segment of the process that creates the object, even if the object is created in a shared memory segment, the vtbl pointer will be valid only for the process that created the object. This implies that other processes that attempt to call a virtual function of a shared object will crash (unless the virtual table for different processes happens to be laid out at the same virtual address, which may not be guaranteed by the operating system). This is the main reason for considering alternatives to dynamic binding for sharing objects between processes.
  1. Pointers to C++ objects that are created in shared memory will be valid in different processes only if they all attach the shared memory segments to the same virtual addresses, but this may not be guaranteed by the operating system (the OS may provide the means to suggest a virtual address and segment size, and it may map the process to that address only if that virtual address is not already in use). The solution is to use offsets to the virtual address that maps to the shared memory for the process and to create pointers to objects at runtime by adding the offsets to the base virtual address. Similarly, rather than having member pointers in C++ classes, we would need to have member offsets (to the base virtual address that maps to shared memory).
  1. The compiler will allocate static data members of a class on the default data segment of a process, so different processes will have a different copy of those members. This may very well be the intended design of the class, but if a single copy of the static data members is needed, the data members should be replaced with offsets to a base virtual address that is mapped to shared memory.
  1. With different processes invoking methods of a shared object simultaneously, they can interfere with each other, resulting in data corruption. IPC mechanisms must be used to provide for mutual exclusion.

Curiously Recurring Template Pattern 

The "Curiously Recurring Template Pattern" (CRTP) is a commonly used alternative to dynamic binding. CRTP is used to implement static polymorphism (aka simulated dynamic binding) [Wikipedia]. Static polymorphism achieves a similar effect to the use of virtual functions, allowing the overloaded functions in the derived classes to be selected at compile time rather than at run time. Using CRTP, a "Derived" class inherits a "Base<Derived>" template class where the Base class implements the Derived class’ interface functions by typecasting the object and calling its interface member function. In order to properly delete instances of derived classes, the Base class is first derived from a general Deletor class which defines a virtual destructor. The virtual destructor provides deletion of derived objects through a pointer to the base class. 

C++
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 */
};

Without using a general base class like Deletor as it is done here, the derived classes cannot be stored heterogeneously as each CRTP base class is a unique type. Base<Derived1> and Base<Derived2> are unrelated classes, so even though these objects can now be stored heterogeneously in a container of base Deletor* objects, they cannot be iterated to provide runtime polymorphism and generically invoke the object’s method (e.g., DoIt() in the example above). CRTP is great for applications where clients need to create a single type of derived class.

Simulated C++ Interface Template Pattern

I am taking a different approach, which is a Simulated C++ Interface Template Pattern (I’ll simply refer to it as SITP). The SITP pattern presented here relies on static template member functions rather than template classes. This pattern requires the creation of a base class that defines static template member functions that will access the derived classes interface member functions. The goal is to be able to add objects of derived classes into a container, and later access them generically by iterating the container and calling each object’s interface member functions without the need to know any details about the object’s type; all we need to know is that the object is derived from a common base class that defines interfaces to a set of member functions that obey a specific function calling pattern.

Let’s assume that we need to have a class hierarchy with a dynamically bound method "int siRun( int& )". We then create a RunnableInterface base class with a static template member function Run_T, and a virtual destructor to provide proper derived object destruction through the base class pointer.

Run_T(..) has the same function protocol as the required siRun(..) member function but with an additional parameter up front, which is a pointer to the object on which the static template function operates. We then define a member variable that is a pointer to the member function, a constructor that initializes it to null, a template member function (Init) to set the member variable to point to the correct implementation of the static member function (&Run_T<T>) and a public member function implementation (siRun) with the same parameters and return type that the static template member function (Run_T) has, but excluding the first parameter (which is a pointer to the object) to invoke the static template function indirectly via the member variable.

C++
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 ); }
};
Notice that the pointer member variable is not tied in any way to a template argument typename T. A generic "void* const pObj" pointer is used to provide a RunnableInterface class instantiation that is independent from the derived class’ type. You might say at this point, that this will not help with sharing objects because we now have a pointer member which may not be valid across process boundaries, but bear with me for now and I will address this later. Also, notice that the classes for such shared objects can still have virtual methods as long as these methods are invoked only by the process that created the objects; the RunnableInterface class has a virtual destructor that provides deletion of derived objects through a pointer to the base class; this works with shared objects as long as the process that creates the objects is also responsible for their destruction.

But if the derived class unintentionally suppresses a definition of one of the interface member functions (e.g., siRun()), its corresponding static_cast in the template function will not fail at compile time, and the application will eventually crash at runtime when an attempt to call the undefined interface member function is made. Fortunately the SFINAE technique (Substitution Failure Is Not An Error) can be used to check at runtime that the interface member function does exist [Wikipedia]; the "CreateMemberFunctionChecker" macro implements a struct that resolves a static "value" at compile time for the specified class to later check at run time if the interface member function is defined for that class. The corresponding "CheckMemberFunction" macro can then be called by Init() to assert the existence of the interface member function at run time. The FNNAME argument in both macros must specify the member function name (e.g. "siRun" in the example discussed here); the second argument in "CheckMemberFunction" must specify the member function prototype using the T typename (e.g., int (T::*)(int&) for the siRun example discussed here).

C++
// 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 ); }

So using these macros, here’s the complete RunnableInterface base class.

C++
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 ); }
}; 

This class can now become a base class to classes that overload an int siRun( int& k ) function that implements behavior that is specific to the derived class, and all we have to do in the derived class’ constructor is invoke "Init<Derived>();" to connect the derived class’ int Derived::siRun( int& k ) member to the base class static template function (Run_T) as demonstrated below. 

C++
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>(); }
};

The "friend RunnableInterface;" is needed to give access to the template functions that are defined within the RunnableInterface class while keeping the scope of the interface member functions private or protected.

Now we can iterate a heterogeneous container of RunnableInterface* objects to access the overloaded functions (which cannot be done with CRTP) as demonstrated below:  

C++
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;
}

But wait!!! The RunnableInterface base class has a member variable that is a pointer to a function. This is fine if we are running in a single process and we want to use this pattern for purposes other than sharing objects across process boundaries. It might even work with some operating systems with objects placed in shared memory if the OS assigns the same code virtual addresses to multiple instances of the same program. But to guarantee that it will work across process boundaries, we really should be using offsets to the module load address instead of pointers to functions. In Windows, GetModuleHandle() returns a module handle that is the same as the load address of the module [MODULEINFO structure]. The pointers to the functions can then be calculated at runtime to generate addresses that are valid for the calling process. The shareable RunnableInterface base class for Windows is shown below.

C++
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

The code example in listings 1, 2 and 3 demonstrates how simple it is to set the mechanism in motion and to share C++ objects across process boundaries. It was built with Qt Creator 5.0.2 - MinGW 4.7 32 bit as well as with Visual Studio 2010 and tested under Windows XP and Windows 7 64 bit.

When building with MinGW, the following DLLs are required in order to run the generated test.exe: libgcc_s_sjlj-1.dll, libstdc++-6.dll and libwinpthread-1.dll.

Image 1

The testiface.h file shown in listing 1 presents the TestInterface class which defines an interface to the following member functions: 

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

testiface.h – Listing 1
C++
#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

The testclasses.h file shown in Listing 2 demonstrates the creation of three derived classes, Base (which derives from TestInterface), DerivedOnce (which derives from Base), and DerivedTwice ( which derives from DerivedOnce). For each of these classes, its constructor makes a call to TestInterface::Init<T>() where T is replaced by the class’ name. This is all that is required from derived classes in order to make the TestInterface mechanism work. In each of these classes, the TestInterface is made a friend class, just for the purpose of allowing the derived class’ interface functions siRun, siReset, and siSayHello to be in private (or protected) scope to everyone else.

testclasses.h – Listing 2
C++
#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

The main.cpp file shown in listing 3, relies on Windows API calls to demonstrate:

  1. The instantiation of objects in shared memory for Base, DerivedOnce, and DerivedTwice classes by an "OWNER" process, and
  2. The access of objects in shared memory by a "CLIENT" process.

In both cases, the objects are placed in a list, and a loop is then created to access them as TestInterface* objects, to generically invoke the siRun, siReset, and siSayHello interface member functions. Multiple instances of the program can be run to demonstrate how objects are successfully shared across process boundaries. Just run the program and keep it running without hitting a key; the "OWNER" (creator of the shared memory and objects allocated in it) will be initially identified in the console window; then run additional instances of the program; the "CLIENT" will be initially identified in the console window and you should see exactly the same output (except for the virtual address for the shared memory in the process, which may or may not be the same), but here we are just accessing the objects that are in shared memory and calling the same code from a different process. To properly terminate the programs, first acknowledge the "CLIENT" program instances and then acknowledge the "OWNER" program instance (which invokes the objects’ destructors).

Try commenting out one of the interface member functions, for example DerivedTwice::siSayHello(), then rebuild the application program; an attempt to run will cause an assertion to fail with an error message (due to the call to CheckMemberFunction( siSayHello, void (T::*)() ) in TestInterface::Init<T>()):

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

Try changing the definition of siSayHello in DerivedTwice to have an argument (such as "double d"), then attempt to rebuild the application program; a compile time error will be generated, due to the call to TestInterface::Init<DerivedTwice>() in the DerivedTwice constructor, when the compiler fails to find a matching function for a call to DerivedTwice::siSayHello():

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

main.cpp – Listing 3
C++
#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;
} 

Summary

The technique presented by this article demonstrates a pattern that can be used in C++ classes to eliminate virtual functions while still providing runtime dynamic binding with virtual-function-like behavior. The pattern provides the ability to iterate a heterogeneous container of instances of these classes and generically invoke the overloaded functions. By eliminating virtual functions it becomes possible to share C++ objects while retaining the ability to provide dynamic binding across process boundaries. While the SITP syntax may not be ideal (i.e. not as simple as declaring a “virtual” function), it serves a purpose that is not addressed by the language and it functionally provides runtime dynamic binding as virtual functions do. 

References

History 

  • 7 June, 2013 - Initial release.
  • 6 August, 2013 - Corrected code to compile with MSVC. Added sitp.zip download link.
  • 5 January, 2014 - Modified Introduction and Summary sections. Added note after definition of "RunnableInterface class" regarding use of virtual destructor and virtual functions in general. Added History section. Corrected typo and minor formatting. Added reference to MSDN article. Added "Sharing C++ Objects" header and "Curiously Recurring Template Pattern" header to existing sections. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)