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.

|