This technique employs only single inheritance, where the object class itself inherits from IUnknown and implements those functions to control the object as a whole. Each additional interface is implemented as a separate C++ interface implementation class that singly inherits from the appropriate interface. Each separate class holds a backpointer to the full object class (with no AddRef because this is a case of nested lifetimes) and delegates all IUnknown calls from its interface to those of the actual object. Thus, reference counting and QueryInterface are centralized in the full object class, as should be the case. In general, the interface implementation classes use the backpointer to get at centralized object variables, so they are commonly declared as friend classes to the object class.
To implement an object with ISampleOne and ISampleTwo, we would declare classes as found in OBJECT1.H:
class CImpISampleOne;
typedef CImpISampleOne *PCImpISampleOne;
class CImpISampleTwo;
typedef CImpISampleTwo *PCImpISampleTwo;
//The C++ class that manages the actual object
class CObject1 : public IUnknown
{
friend CImpISampleOne;
friend CImpISampleTwo;
private:
DWORD m_cRef; //Object reference count
PCImpISampleOne m_pImpISampleOne;
PCImpISampleTwo m_pImpISampleTwo;
public:
CObject1(void);
~CObject1(void);
BOOL Init(void);
//IUnknown members
STDMETHODIMP QueryInterface(REFIID, PPVOID);
STDMETHODIMP_(DWORD) AddRef(void);
STDMETHODIMP_(DWORD) Release(void);
};
typedef CObject1 *PCObject1;
class CImpISampleOne : public ISampleOne
{
private:
DWORD m_cRef; //For debugging
PCObject1 m_pObj; //Backpointer for delegation
public:
CImpISampleOne(PCObject1);
~CImpISampleOne(void);
//IUnknown members
STDMETHODIMP QueryInterface(REFIID, PPVOID);
STDMETHODIMP_(DWORD) AddRef(void);
STDMETHODIMP_(DWORD) Release(void);
//ISampleOne members
STDMETHODIMP GetMessage(LPTSTR, UINT);
};
class CImpISampleTwo : public ISampleTwo
{
private:
DWORD m_cRef; //For debugging
PCObject1 m_pObj; //Backpointer for delegation
public:
CImpISampleTwo(PCObject1);
~CImpISampleTwo(void);
//IUnknown members
STDMETHODIMP QueryInterface(REFIID, PPVOID);
STDMETHODIMP_(DWORD) AddRef(void);
STDMETHODIMP_(DWORD) Release(void);
//ISampleTwo members
STDMETHODIMP GetString(LPTSTR, UINT);
};
My personal convention is to name the interface classes with CImp<Interface> and the variables in the object that hold pointers to these classes with m_pImp<Interface>. This variable naming distinguishes interface implementations that the object manages from other interface pointers that it might store as well, which I name with m_p<Interface>. These are my conventions: use whatever you like because OLE itself doesn't care about such implementation details.
The Query client creates this object through the CreateObject1 function, which looks a lot like CreateRectEnumeratorCPP. One addition is that after creating an instance of CObject1 with the new operator, the Query client calls CObject1::Init, which explicitly instantiates the interface implementations:
CObject1::CObject1(void)
{
m_cRef=0;
m_pImpISampleOne=NULL;
m_pImpISampleTwo=NULL;
return;
}
§
BOOL CObject1::Init(void)
{
m_pImpISampleOne=new CImpISampleOne(this);
if (NULL==m_pImpISampleOne)
return FALSE;
m_pImpISampleTwo=new CImpISampleTwo(this);
if (NULL==m_pImpISampleTwo)
return FALSE;
return TRUE;
}
The this pointer passed to the interface implementation constructors becomes the backpointer to the object, through which the interface implementations delegate IUnknown calls. Again, the interface implementations do not call AddRef on this pointer because their lifetimes are nested within CObject1's. Thus, the object can simply call delete on its m_pImp* pointers in its destructor to clean up the allocations made inside Init, as shown on the following page.
CObject1::~CObject1(void)
{
DeleteInterfaceImp(m_pImpISampleTwo);
DeleteInterfaceImp(m_pImpISampleOne);
return;
}
The destructor is called from within Release, which calls delete this, as we saw before. In the destructor, the macro DELETEINTERFACEIMP is my own creation. (You'll find it in INC\INOLE.H.) This macro calls delete on the given pointer and sets that pointer to NULL. I have also defined a macro, RELEASEINTERFACE, that calls Release on a pointer and sets it to NULL:
#define DeleteInterfaceImp(p)\
{\
if (NULL!=p)\
{\
delete p;\
p=NULL;\
}\
}
#define ReleaseInterface(p)\
{\
if (NULL!=p)\
{\
p->Release();\
p=NULL;\
}\
}
I have found that setting a pointer to NULL after you believe you have deleted it or released it for the last time is very useful for debugging, as it easily exposes interface calls after the interface becomes invalid. I recommend that you use this technique in your own work as well: it will save you some head banging.
Now to the real purpose of our discussion—the implementation of QueryInterface. CObject1 effectively supports three interfaces: IUnknown, ISampleOne, and ISampleTwo. Each interface pointer comes from a different source:
STDMETHODIMP CObject1::QueryInterface(REFIID riid, PPVOID ppv)
{
*ppv=NULL;
//IUnknown comes from CObject1.
if (IID_IUnknown==riid)
*ppv=this;
//Other interfaces come from interface implementations.
if (IID_ISampleOne==riid)
*ppv=m_pImpISampleOne;
if (IID_ISampleTwo==riid)
*ppv=m_pImpISampleTwo;
if (NULL==*ppv)
return ResultFromScode(E_NOINTERFACE);
((LPUNKNOWN)*ppv)->AddRef();
return NOERROR;
}
In other words, this is the IUnknown pointer, whereas m_pImpISampleOne and m_pImpISampleTwo are the others. If the client calls any IUnknown function through these latter two interface pointers, the calls are delegated to CObject1's implementation:
CImpISampleOne::CImpISampleOne(PCObject1 pObj)
{
m_cRef=0;
m_pObj=pObj;
return;
}
STDMETHODIMP CImpISampleOne::QueryInterface(REFIID riid, PPVOID ppv)
{
return m_pObj->QueryInterface(riid, ppv);
}
DWORD CImpISampleOne::AddRef(void)
{
++m_cRef;
return m_pObj->AddRef();
}
DWORD CImpISampleOne::Release(void)
{
--m_cRef;
return m_pObj->Release();
}
The same thing happens in CImpISampleTwo. Do note that these implementations maintain their own interface reference counts simply for debugging. If you wanted to—and you are not supporting aggregation—you could have CObject1::QueryInterface create the interface implementations when necessary and have those implementations destroy themselves in Release. The technique is not allowed in aggregation, however, because there you have to ensure validity of all interface pointers until the object as a whole is destroyed, as we'll see shortly.
The primary advantage of this technique is that everything is explicit: you can trace when and where interfaces are instantiated, and you can watch everything that happens. For this reason, and because it is more readily understood by a C++ neophyte, I've used this technique in almost every multiple-interface sample in this book. Its biggest drawback is that it's rather verbose; the other techniques don't require as much source code. The extra step of IUnknown delegation is also a minor but usually insignificant performance degradation, and this technique generally uses more memory than others do.