COM+ has been criticized for not supporting implementation inheritance. But inheritance is just one technique that enables code reuse—it is by no means the only technique. Language-based implementation inheritance often comes with many of its own problems; witness the fragile base class problem in C++. COM+ novices often claim that containment and aggregation serve as the reuse mechanisms of COM+. Nothing could be further from the truth. Code reuse in COM+ happens by factoring your functionality into interfaces that are implemented by objects, which can then be reused by any application, written in any language, running anywhere that COM+ is supported.
So what are containment and aggregation needed for? They are simply techniques for merging the identities of two or more objects. These techniques can be useful when you want to transparently add some services to an object without modifying its source code. Recall that the IUnknown interface pointer of every object defines its identity. This means that if you want to merge the functionality of two distinct objects, you must make sure that the new super-object represents only a single identity to the client. In other words, the object must adhere to the rules of identity we discussed previously.
The basic idea of containment, in which one object completely reimplements the interfaces of another, is shown in Figure 2-8.
Figure 2-8. Delegating method calls using containment.
The containment technique simply follows through on the idea that any COM+ object can be a client of another object; it requires no special support from COM+. Here we see that one object pretends to support an interface when it is really acting only as a mediator by passing along the client's request to a third party. Using multiple inheritance in C++, the container object implements both the IMultiply and ISum interfaces, as shown in the following class declaration:
class CContainer : public IMultiply, public ISum { // Methods and data go here... }; |
The IMultiply interface works similarly to the ISum interface except that instead of adding two numbers, it returns their product. Using IDL syntax, the IMultiply interface is expressed as follows:
[ object, uuid(10000011-0000-0000-0000-000000000001) ] interface IMultiply : IUnknown { HRESULT Multiply(int x, int y, [out, retval] int* retval); } |
In the usual fashion, the container object actually implements the IMultiply interface using its Multiply method. As part of its construction sequence, however, the container object also instantiates the CLSID_InsideCOM object, as shown in the code below.
HRESULT CContainer::Init() { return CoCreateInstance(CLSID_InsideCOM, NULL, CLSCTX_INPROC_SERVER, IID_ISum, (void**)&m_pSum); } |
There is never any danger that the identity laws of COM+ will be violated because the outer object never hands out a direct ISum interface pointer to the client. Instead, the outer object's QueryInterface method returns a pointer to its own implementation of ISum:
HRESULT CContainer::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IMultiply*)this; // Choose left-most base else if(riid == IID_ISum) *ppv = (ISum*)this; else if(riid == IID_IMultiply) *ppv = (IMultiply*)this; else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } |
Notice that the request for the IUnknown interface pointer in the code above is satisfied by casting the this pointer to an IMultiply interface pointer. This coercion is required because the CContainer class uses multiple inheritance to implement the IMultiply and ISum interfaces, as shown in Figure 2-9.
Figure 2-9. Multiple inheritance in action.
Because IMultiply is derived from IUnknown and ISum is derived from IUnknown, we have run into one of the snags of multiple inheritance in C++. Luckily, since both interfaces are abstract base classes, we don't have to worry about duplicate function implementations. Nevertheless, the code that attempts to cast the this pointer into a pointer to IUnknown—a legal operation—runs afoul of the compiler's type checking. The cast is ambiguous because it is not clear whether we want IMultiply's IUnknown or ISum's IUnknown. The truth of the matter is that we couldn't care less, so casting the this pointer to either IMultiply or ISum will work fine.16
Now when the client calls the ISum::Sum method, the container object simply delegates the call to the CLSID_InsideCOM object, as shown here:
HRESULT CContainer::Sum(int x, int y, int* retval) { // Delegate this call to our contained object. return m_pSum->Sum(x, y, retval); } |
This code demonstrates the simplest kind of containment—it does nothing besides delegate to another object. More sophisticated uses of containment might add code before and after delegating the call to the internal object, which might allow the container object to modify the behavior of the internal object in some way. The container object might even decide not to delegate to the internal object under some circumstances. In such cases, the container object is expected to provide the entire behavior necessary to satisfy the client request.
Aggregation is a specialized form of containment supported by COM+. Instead of requiring an object to provide stub methods that delegate to the actual object, as is the case with standard containment, aggregation allows the inner object's interfaces to be exposed directly as if they belong to the outer object (also called the aggregate object). Note that aggregation works only with in-process17 components. The architecture of an aggregate object and its inner object is shown in Figure 2-10.
Figure 2-10. A COM+ object being aggregated.
The outer object implements only the IMultiply interface, as shown in the following class declaration:
class CAggregator : public IMultiply { // Methods and data go here... }; |
While the outer object is being initialized, the inner object is instantiated, as in the containment example in the previous section. In this case, however, the second parameter of the CoCreateInstance call made by the outer object is a pointer to the IUnknown interface of the outer object itself, as shown below. This parameter tells the InsideCOM object that it is being aggregated. On returning from CoCreateInstance, the outer object saves the m_pUnknownInner pointer returned by the inner object for later use in the QueryInterface method.
HRESULT CAggregator::Init() { return CoCreateInstance(CLSID_InsideCOM, (IUnknown*)this, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&m_pUnknownInner); } |
If the object does not support aggregation, it returns the CLASS_E_NOAGGREGATION error. If it does support aggregation, the first interface pointer requested by the outer object must be IUnknown or the IClassFactory::CreateInstance method must return an error, as shown in the next code snippet. This is required because aggregatable objects have two implementations of the IUnknown interface: one for use solely by the outer object and another for use by clients of the outer object.
HRESULT CFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv) { if(pUnknownOuter != NULL && riid != IID_IUnknown) return CLASS_E_NOAGGREGATION; CInsideCOM* pInsideCOM = new CInsideCOM(pUnknownOuter); if(pInsideCOM == NULL) return E_OUTOFMEMORY; // riid is probably IID_IUnknown. HRESULT hr = pInsideCOM-> QueryInterface_NoAggregation(riid, ppv); pInsideCOM->Release_NoAggregation(); return hr; } |
Notice that in the code above, the pUnknownOuter parameter provided by the CreateInstance method is passed to the CInsideCOM class's constructor. In the constructor, the object determines whether it is being created as a stand-alone object or as part of an aggregate object, as shown in the following code. If the pointer to the outer object's IUnknown interface is NULL, the object is being created as a stand-alone object and thus sets the m_pUnknownOuter pointer to its own IUnknown. Otherwise, it sets m_pUnknownOuter to the outer object's IUnknown, also called the controlling unknown.
CInsideCOM::CInsideCOM(IUnknown* pUnknownOuter) : m_cRef(1) { g_cObjects++; if(pUnknownOuter != NULL) // We're being aggregated. // No AddRef! m_pUnknownOuter = pUnknownOuter; else // Standard usage m_pUnknownOuter = (IUnknown*)(INoAggregationUnknown*)this; } |
The inner object saves the m_pUnknownOuter pointer without calling AddRef; this is normally prohibited by the reference counting rules governing IUnknown. If the inner object were to AddRef the outer object and the outer object were to AddRef the inner object, an unbreakable reference counting cycle would occur. With aggregation, the inner object holds a non-reference-counted interface pointer. This works because the outer object is responsible for destroying the inner object before it exits, so the m_pUnknownOuter pointer is always valid during the lifetime of the inner object.
Although the outer object does not implement the ISum interface, it exposes this interface via its IUnknown::QueryInterface method. If the client requests the ISum interface from the outer object, the IUnknown::QueryInterface call is simply delegated to the inner object, as shown in boldface in the following code. This technique allows the client to have a direct pointer to the inner object (without being aware of it) instead of having to go through a stub function as in the containment example. Of course, in the containment example, pre-delegation and post-delegation processing is possible. These types of processing are not available with aggregation.
HRESULT CAggregator::QueryInterface(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (IUnknown*)this; else if(riid == IID_ISum) return m_pUnknownInner->QueryInterface(riid, ppv); else if(riid == IID_IMultiply) *ppv = (IMultiply*)this; else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } |
One problem with aggregation is that it is easy to break the COM+ object identity rules. The whole point of aggregation is to make interfaces implemented by another object a part of your object's identity. The trick to pulling this off is to never let the client know that your component is melding two identities into one. For example, imagine a client that retrieves a pointer to the IMultiply interface of the object and then calls QueryInterface for the ISum interface. The outer object delegates the QueryInterface call to the inner object and then returns a direct pointer to the ISum interface—so far, so good. Using the ISum pointer, the client later calls QueryInterface again, this time to retrieve the IMultiply interface. This call fails because the inner object does not know about the IMultiply interface supported by the aggregate object.
This failure violates the symmetric rule of QueryInterface, which specifies that if a client holding a pointer to one interface queries successfully for another interface, the client must be able to call QueryInterface through the new pointer for the first interface. Thus, the client suddenly finds that what it thought was the single identity of a COM+ object actually has a split personality.
To solve this insidious problem, objects that support aggregation end up implementing two sets of IUnknown methods. One set simply delegates to the outer object's IUnknown interface, and the other set does the typical work of IUnknown. The delegating set of IUnknown methods, shown below, is used by all custom interfaces implemented by the inner object and is the only one seen by clients of the outer object. This ensures that, from the perspective of the client, the identity of the object is preserved.
ULONG CInsideCOM::AddRef() { return m_pUnknownOuter->AddRef(); } ULONG CInsideCOM::Release() { return m_pUnknownOuter->Release(); } HRESULT CInsideCOM::QueryInterface(REFIID riid, void** ppv) { return m_pUnknownOuter->QueryInterface(riid, ppv); } |
The second, nondelegating, set of IUnknown methods is available only to the outer object for use in implementing its QueryInterface method. The only way for the outer object to obtain a pointer to the nondelegating version of IUnknown is through the initial call to IClassFactory::CreateInstance. This is why the outer object must request the IUnknown interface via the CreateInstance method.
You might expect that with two implementations of IUnknown in one object, naming collisions would occur. You can avoid this problem by giving the second implementation of IUnknown a new name: INoAggregationUnknown. Because COM+ is a binary standard, it is not concerned with the name given to an interface—only with its v-table layout. Therefore, the INoAggregationUnknown interface is defined with a v-table structure identical to that of the real IUnknown:
interface INoAggregationUnknown { virtual HRESULT __stdcall QueryInterface_NoAggregation( REFIID riid, void** ppv)=0; virtual ULONG __stdcall AddRef_NoAggregation()=0; virtual ULONG __stdcall Release_NoAggregation()=0; }; |
An aggregatable object can thus implement two versions of the IUnknown interface: one that delegates to the outer object's IUnknown interface and one that does the typical work of returning interface pointers and reference counting. When the object is not being aggregated, only the nondelegating INoAggregationUnknown interface is used, as shown below. Notice that the pointer returned by QueryInterface is cast to an IUnknown pointer, and then AddRef is called. This ensures that the correct version of AddRef is called for the specific interface pointer being returned: requests for the inner object's IUnknown interface invoke the nondelegating AddRef, while requests for the ISum interface invoke the delegating version of AddRef.
HRESULT CInsideCOM::QueryInterface_NoAggregation(REFIID riid, void** ppv) { if(riid == IID_IUnknown) *ppv = (INoAggregationUnknown*)this; else if(riid == IID_ISum) *ppv = (ISum*)this; else { *ppv = NULL; return E_NOINTERFACE; } ((IUnknown*)(*ppv))->AddRef(); return S_OK; } ULONG CInsideCOM::AddRef_NoAggregation() { return ++m_cRef; } ULONG CInsideCOM::Release_NoAggregation() { if(--m_cRef != 0) return m_cRef; delete this; return 0; } |