We are going to provide a shared pure abstract interface that
will be used by the client and made concrete by our (hidden)
implementation class. The benefits of this interface-based
programming are as follows.
- The "name-mangling" problem is solved because all
client access to the methods in the implementation class is
resolved into an offset into a vtable. This means that, in fact,
the client and server could use totally different method
names in their interface definitions as long as the semantics of
the methods did not change and neither did any other aspect of
their syntax.
- The problem of different object implementations
having varying "binary layout" also goes away because client's
access the implementation class indirectly through a vtable and no
details of the implementation class are known to the client.
Implementation details can change as long as the interface remains
constant. If we restrict ourselves to a single inheritance
hierarchy when defining the interface then we are guaranteed that
the memory layout of the vtable will look the same across all
compilers on a given platform. We can also restrict ourselves to
the most popular C++ calling convention for a given platform in
order to guarantee interoperability.
- If we don't change the interface we can upgrade the
implementation code without affecting the client.
We are going to put this class implementation in a DLL.
| Defining and using an interface
|
So let's redefine our indispensable string class.
Header file istring.h, shared by the implementation class and the
client. This represents the only point of contact between the client
and the implementation.
struct
IString { virtual const char*Find(const
char *psz)=0; virtual int
Length()=0; };
Header file mystring.h, used by the concrete implementation class
but never seen by the client. This defines part of the concrete
implementation class that implements the interface.
#include "istring.h"
class CMyString : public
IString { private: char
*m_psz; public: CMyString(const char *
psz); ~CMyString();
const char*Find(const char *psz); int
Length(); };
Source file mystring.cpp, coded by the concrete implementation
class but never seen by the client. This defines the other part of
the concrete implementation class that implements the interface.
#include "mystring.h"
CMystring::CMyString(const char *
psz) : m_psz( new char[psz ? strlen(psz)+1 :1])
{ if ( psz
)
strcpy(m_psz,psz);
else m_psz[0] =
0; } CMyString::~CMyString() { delete []
m_psz; } const char*CMyString::Find(const char *psz)
{ return strstr(m_psz,psz); } int
CMyString::Length() { return
strlen(m_psz); }
Source file stringclient.cpp, implemented by the client in order
to use the string class through its IString interface.
#include "istring.h"
void main()
{ IString *p;
// how do we
create the implementation class and get it to give back an IString
reference? const char*psz =
p->Find("llo"); int n =
p->Length(); };
| Out-of-band technique for creating
objects |
The client cannot use the new
operator to create the string concrete implementation class because
the CMyString constructor is hidden (for a good reason - we don't
want the client knowing any implementation details). Neither can
they use new to instantiate an IString because it is abstract. What
we need to do is push the creation of the implementation class back
behind the DLL boundary. This could be achieved by implementing a
DLL function that knows how to create the implementation class and
give the client back an IString reference.
Header file istring.h.
extern "C"
_declspec(dllexport)IString *CreateString(const char
*psz); typedef IString * (*PfnCreateString)(const char
*psz);
Source file mystring.cpp.
extern "C" IString
*CreateString(const char *psz) {
return new CMyString(psz); }
Source file stringclient.cpp.
#include "istring.h"
void main()
{ IString *p;
p =
CreateString("Hello"); if (p)
{ const char*psz =
p->Find("llo"); int
n = p->Length(); } };
This single exported "object creation" function has no need to be
name-mangled. It also provides the possibility of choosing the DLL,
and thus implementation of IString, dynamically at runtime. In which
case the client would look like this:
Source file stringclient.cpp.
#include "istring.h"
void main()
{ IString *p; HANDLE h =
LoadLibrary("c:\\temp\mystring.dll"); if
(NULL!=h) {
PfnCreateString pfn =
(PfnCreateString)GetProcAddress(h,"CreateString");
if (pfn)
{
p = pfn("Hello");
if (p)
{
const char*psz =
p->Find("llo");
int n =
p->Length();
}
} // Be careful about
calling FreeLibrary.
// It will summarily dismiss the DLL from
memory. // You may
still have objects in
memory. //
} };
| A technique for deleting objects
|
We also have a matching problem with deletion of the object. The
details of object deletion must be encapsulated within the DLL. It
is tempting to mark the interface as having a virtual destructor,
and rely on that to destroy the implementation class when we call
delete on the interface. Unfortunately that won't work because the
implementation of virtual destructors is one of the things that
differ from compiler to compiler and affect the binary layout of the
vtable. We'll just have to define a pseudo destructor of our own as
part of the interface definition.
Header file istring.h
struct
IString { virtual void
Delete()=0; virtual const char*Find(const char
*psz)=0; virtual int
Length()=0; };
Header file mystring.h.
#include "istring.h"
class CMyString : public
IString { private: char
*m_psz; public: CMyString(const char *
psz); virtual
~CMyString(); void
Delete(); const char*Find(const char
*psz); int Length(); };
Source file mystring.cpp.
#include "mystring.h"
void CMyString::Delete()
{ delete this; }
Source file stringclient.cpp.
#include "istring.h"
void main()
{ IString *p;
p =
CreateString("Hello"); if (p)
{ const char*psz =
p->Find("llo"); int
n = p->Length();
p->Delete(); } };
Leaving the virtual destructor in the class definition does not
hurt and protects us in the (unlikely) case that we delete a
CMyString object via a base class reference. So, we have
successfully pushed object creation and deletion behind the DLL
boundary and the only point of contact between the client and the
object is via the interface and one global function exported from
the DLL.
If you would like to write some code to try this out, click here.
Next
we will look at how to evolve the functionality of our class.

|