Multiple
Inheritance
To support multiple
inheritance (MI) of interfaces, ATL provides four separate
interface entry macros. Two are for straight casts and two are for branching casts. A straight cast is a
static_cast that the compiler needs no extra information
to performthat is, there are no ambiguities. On the other hand, a
branching cast is used when a class has several base classes that
all derive from the same base class themselves. Because a straight
cast to the common base class would be ambiguous, the compiler
needs the inheritance branch to follow to resolve the
ambiguity.
Straight
Casting
COM_INTERFACE_ENTRY and COM_INTERFACE_ENTRY_IID
As I mentioned, COM_INTERFACE_ENTRY is
the one you'll use most of the time. Its close cousin is
COM_INTERFACE_ENTRY_IID:
#define COM_INTERFACE_ENTRY_IID(iid, x) \
{ &iid, offsetofclass(x, _ComMapClass), _ATL_SIMPLEMAPENTRY},
This macro enables you to specify the IID
separately from the name of the interface. The classic use of this
macro is to avoid ambiguity. Imagine that you've got two interfaces
that derive from the same base interface:
interface IGlobe : ISphere {};
interface IPlanet : ISphere {};
If you've got a class that derives from both of
these interfaces, the compiler won't know which base class to cast
to if you use COM_INTERFACE_ENTRY for
ISphere:
class CDesktopGlobe :
public CComObjectRootEx<CDesktopGlobe>,
public IGlobe,
public IPlanet {
public:
...
BEGIN_COM_MAP(CDesktopGlobe)
COM_INTERFACE_ENTRY(ISphere) // ambiguous
COM_INTERFACE_ENTRY(IGlobe)
COM_INTERFACE_ENTRY(IPlanet)
END_COM_MAP()
// ISphere methods
...
// IGlobe methods
...
// IPlanet methods
...
};
The problem is more easily seen when you look at
the inheritance hierarchy in Figure 6.1.
Figure
6.1 shows two interfaces that CDesktopGlobe has
inherited from more than once, IUnknown and
ISphere. IUnknown is not a problem because ATL
handles it specially by choosing the first entry in the interface
map (as discussed earlier). ISphere is a problem, though,
because there are two of them, IGlobe and
IPlanet. Each base interface has a separate vptr
that points to a separate vtbl. Even though we've got a
shared implementation of all the methods of ISphere and,
therefore, duplicate entries in both the IGlobe and the
IPlanet vtbls, the compiler needs us to pick one.
COM_INTERFACE_ENTRY_IID enables us to resolve this
ambiguity:
class CDesktopGlobe :
public CComObjectRootEx<CDesktopGlobe>,
public IGlobe,
public IPlanet {
public:
...
BEGIN_COM_MAP(CDesktopGlobe)
COM_INTERFACE_ENTRY_IID(IID_ISphere, IGlobe) // unambiguous
COM_INTERFACE_ENTRY(IGlobe)
COM_INTERFACE_ENTRY(IPlanet)
END_COM_MAP()
...
};
In this case, because we
have shared implementations of the ISphere methods in our
implementation of IGlobe and IPlanet, it doesn't
really matter which one we hand out. Sometimes it matters very
much. COM_INTERFACE_ENTRY_IID is often used when exposing
multiple dual interfaces, each of which derives from
IDispatch. Using IDispatchImpl, we provide base
class implementations of IDispatch that are different
based on the dual interface we're implementing. In fact, any time
you've got multiple implementations of the same interface in the
base classes, you must decide which implementation is the
"default." Imagine another example of a base class interface
implementation that has nothing to do with scripting:
template <typename Base> class ISphereImpl : public Base {...};
Using ISphereImpl looks like this:
class CDesktopGlobe :
public CComObjectRootEx<CDesktopGlobe>,
public ISphereImpl<IGlobe>,
public ISphereImpl<IPlanet> {
public:
...
BEGIN_COM_MAP(CDesktopGlobe)
COM_INTERFACE_ENTRY_IID(IID_ISphere, IGlobe) // Default ISphere
COM_INTERFACE_ENTRY(IGlobe)
COM_INTERFACE_ENTRY(IPlanet)
END_COM_MAP()
...
};
Here's the problem: If the client queries for
IGlobe (or ISphere) and calls ISphere
methods, it gets different behavior than if it were to query for
IPlanet and call ISphere methods. Now the client
has just the kind of order-of-query problem that the laws of COM
identity were built to prohibit. Multiple implementations of
the same base interface
clearly violate the spirit, if not the letter, of the laws of COM
identity.
Branch
Casting
COM_INTERFACE_ENTRY2 and COM_INTERFACE_ENTRY2_IID
Both COM_INTERFACE_ENTRY2 and
COM_INTERFACE_ENTRY2_IID are simple entries meant for use
with MI:
#define COM_INTERFACE_ENTRY2(x, x2)\
{ &_ATL_IIDOF(x),\
reinterpret_cast<DWORD_PTR>( \
static_cast<x*>( \
static_cast<x2*>( \
reinterpret_cast<_ComMapClass*>(8))))-8, \
_ATL_SIMPLEMAPENTRY},
#define COM_INTERFACE_ENTRY2_IID(iid, x, x2)\
{ &iid,\
reinterpret_cast<DWORD_PTR>( \
static_cast<x*>( \
static_cast<x2*>( \
reinterpret_cast<_ComMapClass*>(8))))-8, \
_ATL_SIMPLEMAPENTRY},
COM_INTERFACE_ENTRY2 is much like
COM_INTERFACE_ENTRY_IID because it enables you to resolve
the problem of multiple bases:
class CDesktopGlobe :
public CComObjectRootEx<CDesktopGlobe>,
public IGlobe,
public IPlanet {
public:
...
BEGIN_COM_MAP(CDesktopGlobe)
COM_INTERFACE_ENTRY2(ISphere, IGlobe) // Use the IGlobal branch
COM_INTERFACE_ENTRY(IGlobe)
COM_INTERFACE_ENTRY(IPlanet)
END_COM_MAP()
...
};
This macro performs its magic by enabling you to
specify two things, the interface to expose (such as
ISphere) and the branch of the inheritance hierarchy to
follow to get to the implementation of that
interface (such as IGlobe). This macro is slightly
different from COM_INTERFACE_ENTRY_IID, in that the
interface is specified by name instead of by IID. If you want to be
very explicit about both, use
COM_INTERFACE_ENTRY2_IID:
class CDesktopGlobe :
public CComObjectRootEx<CDesktopGlobe>,
public IGlobe,
public IPlanet {
public:
...
BEGIN_COM_MAP(CDesktopGlobe)
COM_INTERFACE_ENTRY2_IID(&IID_ISphere, ISphere, IGlobe)
COM_INTERFACE_ENTRY(IGlobe)
COM_INTERFACE_ENTRY(IPlanet)
END_COM_MAP()
...
};
COM_INTERFACE_ENTRY2[_IID] provides no
extra functionality beyond what COM_INTERFACE_ENTRY[_IID]
provides, so I tend to always use the latter.
Handling Name
Conflicts
One of the problems with MI is name collisions.
Imagine the following interfaces:
interface ICowboy : IUnknown {
HRESULT Draw();
};
interface IArtist : IUnknown {
HRESULT Draw();
};
Because both Draw methods have the same
signature, using straight MI requires a single shared
implementation:
// Ace Powell was a cowboy/artist who lived in the western US
// from 1912 to his death in 1978. I'd like to thank Tim Ewald
// for this fabulous example, which I have used to death
// for years.
class CAcePowell :
public CComObjectRootEx<CComSingleThreadModel>,
public ICowboy,
public IArtist
{
public:
BEGIN_COM_MAP(CAcePowell)
COM_INTERFACE_ENTRY(ICowboy)
COM_INTERFACE_ENTRY(IArtist)
END_COM_MAP()
...
STDMETHODIMP Draw() { /* Act as a cowboy or an artist? */ }
};
The implied meaning of Draw is very
different for an artist than it is for a cowboy, so we'd like to be
able to provide two Draw implementations. We can deal with
this predicament in a couple ways. The first solution uses a
Microsoft-specific extension to the C++ language that allows a very
intuitive syntax for disambiguating the Draw
implementation. It's employed as follows:
class CAcePowell :
public CComObjectRootEx<CComSingleThreadModel>,
public ICowboy,
public IArtist {
public:
BEGIN_COM_MAP(CAcePowell)
COM_INTERFACE_ENTRY(ICowboy)
COM_INTERFACE_ENTRY(IArtist)
END_COM_MAP()
...
STDMETHODIMP IArtist::Draw() {
/* Draw like an artist */
return S_OK;
}
STDMETHODIMP ICowboy::Draw() {
/* Draw like a cowboy */
return S_OK;
}
};
By decorating the method with the name of the
interface, the compiler can figure out which Draw method
you are implementing. However, there is an important limitation
with this technique imposed by what I consider a bug in the
compiler: You must place the body of these methods in the class
declaration in the header file. If you try to just put a
declaration in the header file and put the body in the CPP file, it
won't compile.
This syntax is not Standard C++, so don't expect to
use this with non-Microsoft compilers (not that much of the rest of
this book would be useful on non-Microsoft compilers anyway). If
you're implementing clashing methods from multiple interfaces, you
might want to turn to alternative technique to address the problem
of name collision. I call this technique, long known to the C++
community, forwarding
shims.
Forwarding shims rely upon the fact that
although we can't distinguish methods in the most derived class, we
can certainly distinguish the methods in individual base
classes:
struct _IArtist : public IArtist {
STDMETHODIMP Draw() { return ArtistDraw(); }
STDMETHOD(ArtistDraw)() =0;
};
struct _ICowboy : public ICowboy {
STDMETHODIMP Draw() { return CowboyDraw(); }
STDMETHOD(CowboyDraw)() =0;
};
Both _IArtist and _ICowboy are
shim classes that implement the method with the conflicting name
and forward to another pure virtual member function with a unique
name. Because both shims derive from the interface in question, the
interfaces IArtist and ICowboy can still appear
in the interface map without difficulty:
class CAcePowell :
public CComObjectRootEx<CComSingleThreadModel>,
public _ICowboy,
public _IArtist {
public:
BEGIN_COM_MAP(CAcePowell)
COM_INTERFACE_ENTRY(ICowboy)
COM_INTERFACE_ENTRY(IArtist)
END_COM_MAP()
...
STDMETHODIMP ArtistDraw();
STDMETHODIMP CowboyDraw();
};
This trick fills the vtbls for
IArtist and ICowboy with _IArtist::Draw
and _ICowboy::Draw. These functions, in turn, forward to
the more derived class's implementation of ArtistDraw and
CowboyDraw. The forwarding shims remove the name conflict
at the cost of an extra vtable per shim class, an extra
entry per method per vtable, and an extra virtual function
invocation per call. If this extra cost bothers you, remove it
using the standard ATL tricks:
template <typename Deriving>
struct ATL_NO_VTABLE _IArtist : public IArtist {
STDMETHODIMP Draw() {
return static_cast<Deriving*>(this)->ArtistDraw();
}
};
template <typename Deriving>
struct ATL_NO_VTABLE _ICowboy : public ICowboy {
STDMETHODIMP Draw() {
return static_cast<Deriving*>(this)->CowboyDraw();
}
};
class ATL_NO_VTABLE CAcePowell :
public CComObjectRootEx<CComSingleThreadModel>,
public _ICowboy<CAcePowell>,
public _IArtist<CAcePowell> {
public:
BEGIN_COM_MAP(CAcePowell)
COM_INTERFACE_ENTRY(ICowboy)
COM_INTERFACE_ENTRY(IArtist)
END_COM_MAP()
...
HRESULT ArtistDraw();
HRESULT CowboyDraw();
};
Don't Go Off
Half-Cocked. . .
You might think it would be enough to change one
of the names by using only one forwarding shim:
template <typename Deriving>
struct ATL_NO_VTABLE _ICowboy : public ICowboy {
STDMETHODIMP Draw() {
return static_cast<Deriving*>(this)->CowboyDraw();
}
};
class ATL_NO_VTABLE CAcePowell :
public CComObjectRootEx<CComSingleThreadModel>,
public _ICowboy<CAcePowell>,
public IArtist {
public:
BEGIN_COM_MAP(CAcePowell)
COM_INTERFACE_ENTRY(ICowboy)
COM_INTERFACE_ENTRY(IArtist)
END_COM_MAP()
...
HRESULT Draw(); // Use for both IArtist::Draw and
// ICowboy::Draw
HRESULT CowboyDraw(); // Never called!
};
Don't be tempted to try
this. Remember that forwarding shims depend on overriding the
behavior for the same member function name in the base classes. If
you provide an implementation of the function in question with the
same name as the function you're implementing in the forwarding
shim in the base, the forwarding shim function will never be
called. By implementing one of the functions in the deriving class,
you've effectively provided an implementation of both, putting you
right back where you were in the first place.
Interface
Coloring
In the same "sneaky C++ trick" way that
forwarding shims let you fill the appropriate vtbl entries
even if the compiler won't cooperate, ATL supports another
technique called interface
coloring. Interface coloring is based on the idea that two
classes can be layout compatible
but not type compatible. Two
classes are layout compatible if they have the same vtbl
structure: The functions must be in exactly the same order, and the
parameters must be exactly the same. The names, however, may be
different. For example, the following two classes are layout
compatible because they each result in a vtbl with the
same number of methods, and every method at the same offset has the
same signature:
struct ISphere : IUnknown {
STDMETHOD(Rotate)(long nDegrees, long* pnOrientation) =0;
STDMETHOD(Twirl)(long nVelocity) =0;
};
struct IRedSphere {
// Colored IUnknown methods
STDMETHOD(RedQueryInterface)( REFIID riid, void** ppv) =0;
STDMETHOD_(ULONG, RedAddRef)() =0;
STDMETHOD_(ULONG, RedRelease)() =0;
// Uncolored ISphere methods
STDMETHOD(Rotate)(long nDegrees, long* pnOrientation) =0;
STDMETHOD(Twirl)(long nVelocity) =0;
};
However, because IRedSphere does not
derive from ISphere, IRedSphere is not type
compatible: The compiler won't let you pass IRedSphere
where ISphere is expected (without coercion). Cloning the
layout of an interface is known as interface coloring. The layout-compatible
interface is said to be colored
because it is identical to the original, except for the names; that
feature is not important to the runtime behavior of your object,
just as a color is unimportant to the runtime behavior of your car.
The names are used at compile time, though, and enable you to
implement multiple versions of the same interface:
class CDesktopGlobe :
public CComObjectRootEx<CComSingleThreadModel>,
public IRedSphere,
public IGlobe,
public IPlanet {
public:
...
BEGIN_COM_MAP(CDesktopGlobe)
// Expose IRedShere when ISphere is requested
COM_INTERFACE_ENTRY_IID(IID_ISphere, IRedSphere)
COM_INTERFACE_ENTRY(IGlobe)
COM_INTERFACE_ENTRY(IPlanet)
END_COM_MAP()
...
// Colored method implementations
STDMETHODIMP RedQueryInterface(REFIID riid, void** ppv)
{ return GetUnknown()->QueryInterface(riid, ppv); }
STDMETHODIMP_(ULONG) RedAddRef() {
_ThreadModel::Increment(&m_cRefSphere);
return GetUnknown()->AddRef();
}
STDMETHODIMP_(ULONG) RedRelease() {
_ThreadModel::Decrement(&m_cRefSphere);
return GetUnknown()->Release();
}
private:
long m_cRefSphere;
};
By
deriving from IRedSphere, we can provide an implementation
of all the colored methods separately from the uncolored ones. By
coloring the IUnknown methods of IRedSphere, we
can handle IUnknown calls on ISphere separately
from the other implementations of IUnknown by the other
interfaces. In this case, we're using RedAddRef and
RedRelease to keep track of an ISphere-specific
reference count. And even though we expose IRedSphere to
the client when it asks for ISphere, as far as the client
is concerned, it has just an ISphere interface pointer.
Because IRedSphere and ISphere are layout
compatible, as far as COM is concerned, the client is right.
void TryRotate(IUnknown* punk) {
ISphere* ps = 0;
// Implicit AddRef really a call to RedAddRef
if(SUCCEEDED(punk->QueryInterface(IID_ISphere, (void**)&ps))) {
// ps actually points to an IRedSphere*
ps->Rotate();
ps->Release(); // Really a call to RedRelease
}
}
COM_INTERFACE_ENTRY_IMPL and COM_INTERFACE_ENTRY_IMPL_IID
Interface coloring is somewhat interesting in
the same way that a car wreck on the side of the road is
interesting: It can be disconcerting as well and can slow traffic.
Beginning with ATL 3.0, the vast majority of IXxxImpl
classes no longer use interface coloring. In ATL 2.x, interface coloring was used for some, but
not all, of the IXxxImpl classes to perform
interface-specific reference counting. These implementation classes
took the following form:
template <typename Deriving> class IXxxImpl {...};
Instead of deriving from
the interface the class implemented, the implementation class used
interface coloring to make itself layout compatible with the
implemented interface. This enabled each class to implement its own
reference counting but prohibited the use of the simple
COM_INTERFACE_ENTRY macro. Additional macros were provided
to make the necessary entries in the interface map:
#define COM_INTERFACE_ENTRY_IMPL(x) \
COM_INTERFACE_ENTRY_IID(_ATL_IIDOF(x), x##Impl<_ComMapClass>)
#define COM_INTERFACE_ENTRY_IMPL_IID(iid, x) \
COM_INTERFACE_ENTRY_IID(iid, x##Impl<_ComMapClass>)
The interface coloring technique was useful only
if you wanted to track some of the ATL-implemented interfaces.
Beginning with ATL 3.0, ATL uses a more generic
mechanism that tracks reference counts on all
interfaces. Toward that end, all the ATL-implementation classes
actually derive from the interface in question, making the use of
COM_INTERFACE_ENTRY_IMPL and
COM_INTERFACE_ENTRY_IMPL_IID macros unnecessary. All new
and ported code should use COM_INTERFACE_ENTRY or
COM_INTERFACE_ENTRY_IID instead. Old code that used the
IMPL forms of the macros still compiles under the new ATL
and acts appropriately.
|