Implementing Interfaces: Interface Containment
There are at times reasons why you may not want to use multiple inheritance for an object implementation. First, you may not be using C++. That aside, you may want to individually track reference counts on each interface separate from the overall object for debugging or for resource management purposes—reference counting is from a client perspective an interface-specific operation. This can uncover problems in a client you might also be developing, exposing situations where the client is calling AddRef through one interface but matching it with a Release call through a different interface.
The third reason that you would use a different method of implementation is when you have two interfaces with the same member function names with possibly identical function signatures or when you want to avoid function overloading. For example, if you wanted to implement IPersistFile, IPersistStorage, and IPersistStream on an object, you would have to write overloaded functions for the Load and Save members of each which might get confusing. Worse, if two interface designers should happen to define interfaces that have like-named methods with like parameter lists but incompatible semantics, such overloading isn't even possible: two separate functions need to be implemented, but C++ unifies the two method definitions. Note that as in general interfaces may be defined by independent parties that do not communicate with each other, such situations are inevitable.
The other implementation method is to use "interface implementations" which are separate C++ objects that each inherit from and implement one interface. The real object itself singly inherits from IUnknown and maintains (or contains) pointers to each interface implementation that it creates on initialization. This keeps all the interfaces separate and distinct. An example of code that uses the containment policy follows:
class CImpIPersistFile : public IPersistFile {
private:
ULONG m_cRef; //Interface reference count for debugging
//"Backpointer" to the actual object.
class CTextRender * m_pObj;
public:
[Constructor, Destructor]
//IUnknown members for IPersistFile
HRESULT QueryInterface(REFIID iid, void ** ppv);
ULONG AddRef(void);
ULONG Release(void);
//IPersistFile Member overrides
HRESULT Load(char * pszFile, DWORD grfMode);
[Other members]
...
}
class CImpIDataObject : public IDataObject
private:
ULONG m_cRef; //Interface reference count for debugging
//"Backpointer" to the actual object.
class CTextRender * m_pObj;
public:
[Constructor, Destructor]
//IUnknown members for IDataObject
HRESULT QueryInterface(REFIID iid, void ** ppv);
ULONG AddRef(void);
ULONG Release(void);
//IPersistFile Member overrides
HRESULT GetData(FORMATETC *pFE,STGMEDIUM *pSTM);
[Other members]
...
}
class CTextRender : public IUnknown
{
friend class CImpIDataObject;
friend class CImpIPersistFile;
private:
ULONG m_cRef; //Reference Count
char * m_pszText; //Pointer to allocated text
ULONG m_cchText; //Number of characters in m_pszText
//Contained interface implementations
CImpIPersistFile * m_pImpIPersistFile;
CImpIDataObject * m_pImpIDataObject;
//Other internal member functions here
public:
[Constructor, Destructor]
HRESULT QueryInterface(REFIID iid, void ** ppv);
ULONG AddRef(void);
ULONG Release(void);
};
In this technique, each interface implementation must maintain a backpointer to the real object in order to access that object's variables (normally this is passed in the interface implementation constructor). This may require a friend relationship (in C++) between the object classes; alternatively, these friend classes can be implemented as nested classes in CTextRender.
Notice that the IUnknown member functions of each interface implementation do not need to do anything more than delegate directly to the IUnknown functions implemented on the CTextRender object. The implementation of QueryInterface on the main object would appear as follows:
HRESULT CTextRender::QueryInterface(REFIID iid, void ** ppv)
{
*ppv=NULL;
//This code assumes an overloaded == operator for GUIDs exists
if (IID_IUnknown==iid)
*ppv=(void *)(IUnknown *)this;
if (IID_IPersitFile==iid)
*ppv=(void *)(IPersistFile *)m_pImpIPersistFile;
if (IID_IDataObject==iid)
*ppv=(void *)(IDataObject *)m_pImpIDataObject;
if (NULL==*ppv)
return E_NOINTERFACE; //iid not supported.
//Call AddRef through the returned interface
((IUnknown *)ppv)->AddRef();
return NOERROR;
}
This sort of delegation structure makes it very easy to redirect each interface's IUnknown members to some other IUnknown, which is necessary in supporting aggregation as explained in Chapter 6. But overall the implementation is not much different than multiple inheritance and both methods work equally well. Containment of interface implementation is more easily translatable into C where classes simply become equivalent structures, if for any reason such readability is desirable (such as making the source code more comprehensible to C programmers who do not know C++ and do not understand multiple inheritance). In the end it really all depends upon your preferences and has no significant impact on performance nor development.