top header
com as a better c++term

 

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

 
Adding functionality

What if we had forgotten to add a vital function into the IString interface, perhaps to find a character at a given 0-based index within the string? It should really have looked like this:

struct IString
{
    virtual void Delete();
    virtual const char*Find(const char *psz);
    virtual int Length();
    virtual char FindAt(int index);
};

Oh well, we'll just change it by adding the new function to the end of the vtable. The vtable entries are guaranteed to be in the order of definition of the class/struct that defined the vtable. The updated DLL implementation class can derive from the new IString definition instead of the old. When the new DLL is asked to provide an IString implementation it can give out the new one.  Old clients who expect to use the old IString will only expect to use the first 3 entries. However, what happens when new clients, expecting to use the new IString, get an old DLL implementation? Such clients will expect 4 entries in the vtable and get only 3.

Evolving implementations may need to extend the initial base interface and expose enhanced functionality. They may even wish to expose orthogonal functionality. What if we wanted our string to be able to save/load itself to/from some persistent media? It might support an interface like this:

struct IPersist
{
    virtual void Delete();
    virtual void Save(const char *pszFile);
    virtual void Load(const char *pszFile);
};

It is not possible to change an interface definition without breaking existing clients and objects. The solution is to allow objects to implement more than one interface and to allow clients to discover if the object they have at runtime supports a given interface.

Recall the static_cast<type-id>(expression) operator. It is a compile-time operation, usually used to perform "up-casting" to limit the scope of a reference. If the expression is not type-compatible with type-id, compilation will fail.

Often, the type of a polymorphic object needs to be discovered based on type information about the object available at runtime. This often involves "down-casting" to a derived type or "side-casting" to a sibling type. C++ compilers emit runtime type information (RTTI) that describes inheritance relationships and is exposed via the dynamic_cast<type-id>(expression) operator. It allows us to figure out at runtime whether an expression is type-compatible with type-id. The type-id must be a pointer or a reference to a previously defined class type or a "pointer to void."The type of expression must be a pointer, if type-id is a pointer, or an l-value if type-id is a reference. The dynamic_cast operator inspects an object¡¯s type information to find the offset of the (suspected) base class. It returns a valid base reference if expression is type-compatible with type-id (just like static_cast) and returns a null pointer/throws an exception if not (unlike static_cast).

Consider the following code fragment.

struct IA
{
    virtual void xxx();
};

struct IB
{
    virtual void yyy();
};

class AB : public IA, public IB
{
public:
    AB();
    void xxx();
    void yyy();
};

func(AB *pAB) {
    IA *pA = dynamic_cast<IA *>(pAB);
    if (pA)
        pA->xxx();
    IB *pB = dynamic_cast<IB *>(pAB);
    if (pB)
        pB->yyy();
}

We are limited to interfaces as our mechanism for communicating with the string class. If, when we create the string class, we could somehow give out an interface that was derived from multiple other interfaces, we could use RTTI in the same way. Unfortunately we cannot use multiple inheritance in interface definition because it is compiler specific and affects the binary layout of the vtable. For the same reason we cannot use RTTI. The RTTI mechanism relies on type information being stored somewhere by the compiler. It is stored differently by each compiler and this affects the binary layout of the vtable also.

We need the moral equivalent of multiple inheritance and RTTI, but designed into our interfaces in such a way as to not compromise binary compatibility.

Extended functionality

Here's how we might add extended functionality to our class.

Header file istring.h.

struct IString
{
    virtual void Delete()=0;
    // Our Dynamic_cast() method works like C++ RTTI and dynamic_cast<>
    virtual void *Dynamic_cast(const char *psz)=0;
    virtual const char*Find(const char *psz)=0;
    virtual int Length()=0;
};

struct IString2 : public IString
{
    virtual char FindAt(int index)=0;
};

Header file mystring.h.

#include "istring.h"

class CMyString : public IString2
{
private:
    char *m_psz;
public:
    CMyString(const char * psz);
    ~CMyString();
    void Delete();
    void *Dynamic_cast(const char *);
    const char*Find(const char *psz);
    int Length();
    char FindAt(int index);
};

Source file mystring.cpp.

#include "mystring.h"

void *CMyString::Dynamic_cast(const char *psz) {
    if (strcmp(psz,"IString")==0)
        return static_cast<IString *>(this);
    else
    if (strcmp(psz,"IString2")==0)
        return static_cast<IString2 *>(this);
return NULL;
}

char CMyString::FindAt(int index) {
    // code omitted due to laziness
}

Source file stringclient.cpp.

#include "istring.h"

void main() {
    IString *p;

    p = CreateString("Hello");
    if (p) {
        IString2 *p2;

        const char*psz = p->Find("llo");
        int n = p->Length();
        if ((p2=(IString2 *)p->Dynamic_cast("IString2")))
            char c = p2->FindAt(3);
        p->Delete();
    }
};

Orthogonal functionality

Here's how we might add orthogonal functionality to our class.

Header file istring.h.

struct IPersist
{
    virtual void Delete()=0;
    virtual void *Dynamic_cast(const char *psz)=0;
    virtual void Save(const char *pszFile)=0;
    virtual void Load(const char *pszFile)=0;
};

Header file mystring.h.

#include "istring.h"

class CMyString : public IString2, public IPersist
{
private:
    char *m_psz;
public:
    CMyString(const char * psz);
    ~CMyString();
    void Delete();
    void *Dynamic_cast(const char *);
    const char*Find(const char *psz);
    int Length();
    char FindAt(int index);
    void Save(const char *pszFile);
    void Load(const char *pszFile);
};

Source file mystring.cpp.

#include "mystring.h"

void *CMyString::Dynamic_cast(const char *psz) {
    if (strcmp(psz,"IString")==0)
        return static_cast<IString *>(this);
    else
    if (strcmp(psz,"IString2")==0)
        return static_cast<IString2 *>(this);
    else
    if (strcmp(psz,"IPersist")==0)
        return static_cast<IPersist *>(this);
    return NULL;
}

void CMyString::Save(const char *pszFile) {
    // code omitted due to laziness
}

void CMyString::Load(const char *pszFile) {
    // code omitted due to laziness
}

Source file stringclient.cpp.

#include "istring.h"

void main() {
    IString *p;

    p = CreateString("Hello");
    if (p) {
        IString2 *p2;
        IPersist *p3;

        const char*psz = p->Find("llo");
        int n = p->Length();
        if ((p2=(IString2 *)p->Dynamic_cast("IString2")))
            char c = p2->FindAt(3);
        if ((p3=(IPersist *)p->Dynamic_cast("IPersist")))
            p3->Save("c:\\temp\\str.txt");
        p->Delete();
    }
};

If you would like to write some code to try this out, click here.

Next we will look at the lifetime management of the class.


Top of document