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?
- 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.
- 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).
- 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.
- 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.
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() { }
};
class Derived2 : public Base<Derived2> {
...
public:
int DoIt() { }
};
int main() {
Derived1 Obj1;
Derived2 Obj2;
Obj1.Run();
Obj2.Run();
};
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.
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 ); 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).
#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)); \
}
#define CheckMemberFunction( FNNAME, FNPROTOTYPE ) { \
assert( has_member_##FNNAME<FNPROTOTYPE>::value ); }
So using these macros, here’s the complete RunnableInterface
base class.
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 ) {
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.
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:
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.
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 );
unsigned long m_ulRun_T_Offset;
protected:
template <typename T>
void Init() {
CheckMemberFunction( siRun, int (T::*)(int&) );
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 ); 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() : 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.
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
#ifndef TESTIFACE_H
#define TESTIFACE_H
#include <windows.h> // for GetModuleHandle()
#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)); \
}
#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:
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(); }
CreateMemberFunctionChecker( siRun );
CreateMemberFunctionChecker( siReset );
CreateMemberFunctionChecker( siSayHello );
unsigned long m_ulRun_T_Offset,
m_ulReset_T_Offset,
m_ulSayHello_T_Offset;
protected:
template <typename T>
void Init() {
CheckMemberFunction( siRun, int (T::*)() );
CheckMemberFunction( siReset, void (T::*)(int&) );
CheckMemberFunction( siSayHello, void (T::*)() );
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 ); 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 ); 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 ); char* pBaseOffset = (char*) GetModuleHandle(NULL);
PFN_SAYHELLO_T pfnSayHello_T = (PFN_SAYHELLO_T)
(pBaseOffset + m_ulSayHello_T_Offset);
(*pfnSayHello_T)( this ); }
TestInterface() : m_ulRun_T_Offset( 0 ),
m_ulReset_T_Offset( 0 ),
m_ulSayHello_T_Offset( 0 ) {}
virtual ~TestInterface() {}
};
#else
class TestInterface {
private:
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(); }
PFN_RUN_T m_pfnRun_T; PFN_RESET_T m_pfnReset_T; PFN_SAYHELLO_T m_pfnSayHello_T;
CreateMemberFunctionChecker( siRun );
CreateMemberFunctionChecker( siReset );
CreateMemberFunctionChecker( siSayHello );
protected:
template <typename T>
void Init() {
CheckMemberFunction( siRun, int (T::*)() );
CheckMemberFunction( siReset, void (T::*)(int&) );
CheckMemberFunction( siSayHello, void (T::*)() );
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 ); return (*m_pfnRun_T)( this ); }
void siReset( int& k ) {
assert( m_pfnReset_T ); (*m_pfnReset_T)( this, k ); }
void siSayHello() {
assert( m_pfnSayHello_T ); (*m_pfnSayHello_T)( this ); }
TestInterface() : 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
#ifndef TESTCLASSES_H
#define TESTCLASSES_H
#ifndef TESTIFACE_H
#include "testiface.h"
#endif
class Base : public TestInterface {
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:
- The instantiation of objects in shared memory for
Base
, DerivedOnce
, and DerivedTwice
classes by an "OWNER" process, and - 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
#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" );
const HANDLE hMapFile =
CreateFileMapping(
INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BufSize, szName ); if ( hMapFile == NULL ) {
std::cout << "Could not create file mapping object (" <<
GetLastError() << ").\n" << std::endl;
return 1;
}
const bool fFirstProcess = (GetLastError() != ERROR_ALREADY_EXISTS);
const LPCTSTR pBuf =
(LPTSTR) MapViewOfFile(
hMapFile, FILE_MAP_ALL_ACCESS, 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;
pObj1 = new(pBuf1) Base; pBuf1 += sizeof( Base ); pObj2 = new(pBuf1) DerivedOnce;
pBuf1 += sizeof( DerivedOnce ); pObj3 = new(pBuf1) DerivedTwice;
}
else {
std::cout << "CLIENT PROCESS: " << std::endl;
pObj1 = (Base*) pBuf1; pBuf1 += sizeof( Base );
pObj2 = (DerivedOnce*) pBuf1; pBuf1 += sizeof( DerivedOnce );
pObj3 = (DerivedTwice*) pBuf1; }
char szHexBuf[12];
sprintf( szHexBuf, "0x%lx", (unsigned long) pBuf );
std::cout << "pBuf: " << szHexBuf << std::endl << std::endl;
std::list<TestInterface*> list1;
list1.insert( list1.end(), pObj1 );
list1.insert( list1.end(), pObj2 );
list1.insert( list1.end(), pObj3 );
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 ) {
for ( i = list1.begin(); i != list1.end(); i++ ) {
TestInterface* const p = *i;
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.