top header
com as a better c++term

 

home contents introduction com as a better C++ interfaces classes apartments labs lab setup quizzes
 
previousnext

 
Refinements to the interface-based approach
to exposing C++ classes

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.


Top of document