Multiple Inheritance (CObject3)

You are probably thinking that both of the previous techniques are too verbose—they have a lot of source code and still use a bunch of classes to implement one object. Multiple inheritance in C++ is the way to cut out the extra classes and combine all the IUnknown implementations into one. That means no delegation and no backpointers: you have only one object. CObject3 is the multiple-inheritance version of what we've already seen, declared in OBJECT3.H:


class CObject3 : public ISampleOne, public ISampleTwo
{
private:
DWORD m_cRef; //Object reference count

public:
CObject3(void);
~CObject3(void);

//Shared IUnknown members
STDMETHODIMP QueryInterface(REFIID, PPVOID);
STDMETHODIMP_(DWORD) AddRef(void);
STDMETHODIMP_(DWORD) Release(void);

//ISampleOne members
STDMETHODIMP GetMessage(LPTSTR, UINT);

//ISampleTwo members
STDMETHODIMP GetString(LPTSTR, UINT);
};

typedef CObject3 *PCObject3;

This uses a lot less code to declare the object, and because there are no other classes, the total amount of memory is less. You also avoid the extra step of delegation for IUnknown functions.

One important factor to remember, however, is that a pointer to the object is not a direct pointer to any interface, even IUnknown. (The compiler will complain that IUnknown is ambiguous because it is inherited from both the other interfaces.) This means that we must use explicit typecasts in QueryInterface to retrieve the correct pointers for each interface, as in the following:


STDMETHODIMP CObject3::QueryInterface(REFIID riid, PPVOID ppv)
{
*ppv=NULL;

if (IID_IUnknown==riid || IID_ISampleOne==riid)
*ppv=(ISampleOne *)this;

if (IID_ISampleTwo==riid)
*ppv=(ISampleTwo *)this;

if (NULL==*ppv)
return ResultFromScode(E_NOINTERFACE);

((LPUNKNOWN)*ppv)->AddRef();
return NOERROR;
}

Because we don't have an explicit IUnknown, we use ISampleOne as our IUnknown. This is because ISampleOne has an IUnknown vtable itself.

It is important to realize that when multiple inheritance is involved, a typecast operation will change the pointer value. What the preceding code stores in *ppv is not the same as what is stored in this. The reason is that C++ overloads the typecast operators to allocate the right vtable for the interface type, and thus you have to have a different pointer to that vtable as well as a different pointer to that pointer, which is what gets stored in *ppv. If you are unfamiliar with how this works, please refer to the "Multiple Inheritance" section in the APPA.WRI file on the companion CD for more details.

Failure to typecast properly can result in some very strange side effects: because ppv is a void **, you can store any pointer you want in it without complaint from the compiler. This means that if you forget the typecast, you'll effectively give the client an interface pointer that points to the object's vtable, not the interface vtable. In multiple inheritance, this object vtable includes all object member functions, such as the constructor and destructor. I've encountered situations in which my object destructor was called from a client, which confused the heck out of me for a long time. Other people have asked me about this same problem, in which they saw a function being called twice. Any strange symptom involving a call to the wrong function indicates a possible failure to typecast in QueryInterface. Do be careful.

As just described, multiple inheritance has the drawback of difficult debugging, especially when vtables are not created properly. But its primary advantage is that the expression of such an object is much more concise and generally looks cleaner in code. When you involve more than a few interfaces, however, things start to get messy, and the whole technique breaks down if you have two interfaces with identically named member functions, which does occasionally occur. For most situations, multiple inheritance is a great technique for production development projects because of its concise nature. If you are planning to implement an aggregatable object, however, you will need to use one of the other techniques to make a set of IUnknown functions outside those inherited from all the other interfaces. In that case, you'll have the main object delegate its IUnknown functions to some other unknown, either the outer unknown (in aggregation) or the object's own IUnknown (outside of aggregation).