Tear-Off
Interfaces
Although multiple inheritance is preferred when
implementing multiple interfaces, it's not perfect. One of the
problems is something called vptr-bloat. For each interface a class derives
from, that's another vptr per instance of that class.
Beefing up our beach ball implementation can lead to some
significant overhead:
class CBeachBall :
public CComObjectRootEx<CBeachBall>,
public ISphere,
public IRollableObject,
public IPlaything,
public ILethalObject,
public ITakeUpSpace,
public IWishIWereMoreUseful,
public ITryToBeHelpful,
public IAmDepressed {...};
Because each beach ball
implements eight interfaces, each instance has 32 bytes of overhead
on Win32 systems before the reference count or any useful state. If
clients actually made heavy use of these interfaces, that wouldn't
be too high of a price to pay. However, my guess is that most
clients will use beach balls for their rollable and play-thing
abilities. Because the other interfaces will be used infrequently,
we'd rather not pay the overhead until they are used. For this,
Crispin Goswell invented the tear-off interface, which he described
in the article "The COM Programmer's
Cookbook."
Standard
Tear-Offs
A tear-off interface is an interface that'd you
want to expose on demand but not actually inherit from in the main
class. Instead, an auxiliary class inherits from the interface to
be torn off, and instances of that class are created any time a
client queries for that interface. For example, assuming that few
clients will think to turn a beach ball into a lethal weapon,
ILethalObject would make an excellent tear-off interface
for the CBeachBall class. Instead of using
CComObjectRootEx as the base class, ATL classes
implementing tear-off interfaces use the
CComTearOffObjectBase as their base class:
template <class Owner, class ThreadModel = CComObjectThreadModel>
class CComTearOffObjectBase
: public CComObjectRootEx<ThreadModel> {
public:
typedef Owner _OwnerClass;
Owner* m_pOwner;
CComTearOffObjectBase() { m_pOwner = NULL; }
};
CComTearOffObjectBase provides one
additional service, which is the caching of the owner of the tear-off interface. Each tear-off
belongs to an owner object that has torn it off to satisfy a
client's request. The owner is useful so that the tear-off instance
can access member data or member functions of the owner class:
class CBeachBallLethalness :
public CComTearOffObjectBase<CBeachBall,
CComSingleThreadModel>,
public ILethalObject {
public:
BEGIN_COM_MAP(CBeachBallLethalness)
COM_INTERFACE_ENTRY(ILethalObject)
END_COM_MAP()
// ILethalObject methods
STDMETHODIMP Kill() {
m_pOwner->m_gasFill = GAS_HYDROGEN;
m_pOwner->HoldNearOpenFlame();
return S_OK;
}
};
COM_INTERFACE_ENTRY_TEAR_OFF
To use this tear-off implementation, the owner
class uses the COM_INTERFACE_ENTRY_TEAR_OFF macro:
#define COM_INTERFACE_ENTRY_TEAR_OFF(iid, x) \
{ &iid, \
(DWORD_PTR)&ATL::_CComCreatorData< \
ATL::CComInternalCreator< ATL::CComTearOffObject< x > > \
>::data, \
_Creator },
_CComCreatorData is just a sneaky trick
to fill in the dw enTRy of the interface entry with a
function pointer to the appropriate creator function. The creator
function is provided by CComInternalCreator, which is
identical to CComCreator except that it calls
_InternalQueryInterface to get the initial interface
instead of QueryInterface. This is necessary because, as I
show you soon, QueryInterface on a tear-off instance
forwards to the owner, but we want the initial interface on a new
tear-off to come from the tear-off itself. That is, after all, why
we're creating the tear-off: to expose that interface.
The pFunc entry
COM_INTERFACE_ENTRY_TEAR_OFF makes is the first instance
of a nonsimple entry so far in this chapter and, thus, the first
macro we've seen that cannot be used as the first entry in the
interface map. The _Creator function is a static member of
the CComObjectRootBase class that simply calls the Creator
function pointer held in the dw parameter:
static HRESULT WINAPI
CComObjectRootBase::_Creator(void* pv, REFIID iid,
void** ppv, DWORD_PTR dw) {
_ATL_CREATORDATA* pcd = (_ATL_CREATORDATA*)dw;
return pcd->pFunc(pv, iid, ppv);
}
The most derived class
of a tear-off implementation is not CComObject, but rather
CComTearOffObject. CComTearOffObject knows about
the m_pOwner member of the base and fills it during
construction. Because each tear-off instance is a separate C++
object, each maintains its own lifetime. However, to live up to the
laws of COM identity, each tear-off forwards requests for new
interfaces to the owner:
template <class Base>
class CComTearOffObject : public Base {
public:
CComTearOffObject(void* pv) {
ATLASSERT(m_pOwner == NULL);
m_pOwner = reinterpret_cast<Base::_OwnerClass*>(pv);
m_pOwner->AddRef();
}
~CComTearOffObject() {
m_dwRef = -(LONG_MAX/2);
FinalRelease();
#ifdef _ATL_DEBUG_INTERFACES
_AtlDebugInterfacesModule.DeleteNonAddRefThunk(
_GetRawUnknown());
#endif
m_pOwner->Release();
}
STDMETHOD_(ULONG, AddRef)() {
return InternalAddRef();
}
STDMETHOD_(ULONG, Release)() {
ULONG l = InternalRelease();
if (l == 0)
delete this;
return l;
}
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {
return m_pOwner->QueryInterface(iid, ppvObject);
}
};
To use a tear-off, the owner class adds an entry
to its interface map:
class CBeachBall :
public CComObjectRootEx<CBeachBall>,
public ISphere,
public IRollableObject,
public IPlaything,
//public ILethalObject, // Implemented by the tear-off
public ITakeUpSpace,
public IWishIWereMoreUseful,
public ITryToBeHelpful,
public IAmDepressed {
public:
BEGIN_COM_MAP(CBeachBall)
COM_INTERFACE_ENTRY(ISphere)
COM_INTERFACE_ENTRY(IRollableObject)
COM_INTERFACE_ENTRY(IPlaything)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_ILethalObject,
CBeachBallLethalness)
COM_INTERFACE_ENTRY(ITakeUpSpace)
COM_INTERFACE_ENTRY(IWishIWereMoreUseful)
COM_INTERFACE_ENTRY(ITryToBeHelpful)
COM_INTERFACE_ENTRY(IAmDepressed)
END_COM_MAP()
...
private:
GAS_TYPE m_gasFill;
void HoldNearOpenFlame();
// Tear-offs are generally friends
friend class CBeachBallLethalness;
};
Because the owner class is no longer deriving from
ILethalObject, each instance is 4 bytes lighter. However,
when the client queries for ILethalObject, we're spending
4 bytes for the ILethalObject vptr in
CBeachBallLethalness, 4 bytes for the
CBeachBallLethalness reference count, and 4 bytes for the
m_pOwner back pointer. You might wonder how spending 12
bytes to save 4 bytes actually results in a savings. I'll tell you:
volume! Or rather, the lack thereof. Because we're paying only the
12 bytes during the lifetime of the tear-off instance and we've
used extensive profiling to determine ILethalObject is
rarely used, the overall object footprint should be smaller.
Tear-Off
Caveats
Before wrapping yourself in the perceived
efficiency of tear-offs, you should be aware of these cautions:
-
Tear-offs are only
for rarely used interfaces. Tear-off interfaces are an
implementation trick to be used to reduce vptr bloat when
extensive prototyping has revealed this to be a problem. If you
don't have this problem, save yourself the trouble and avoid
tear-offs.
-
Tear-offs are for
intra-apartment use only. The stub caches a tear-off
interface for the life of an object. In fact, the current
implementation of the stub manager caches each interface twice,
sending the overhead of that particular interface from 12 bytes to
24 bytes.
-
Tear-offs should
contain no state of their own. If a tear-off contains its
own state, there will be one copy of that state per tear-off
instance, breaking the spirit, if not the laws, of COM identity. If
you have per-interface state, especially large state that you want
to be released when no client is using the interface, use a cached
tear-off.
Cached
Tear-Offs
You might have noticed that every query for
ILethalObject results in a new tear-off instance, even if
the client already holds an ILethalObject interface
pointer. This might be fine for a single interface tear-off, but
what about a related group of interfaces that will be used
together? For example, imagine moving the other
rarely used interfaces of CBeachBall to a single tear-off
implementation:
class CBeachBallAttitude :
public CComTearOffObjectBase<CBeachBall,
CComSingleThreadModel>,
public ITakeUpSpace,
public IWishIWereMoreUseful,
public ITryToBeHelpful,
public IAmDepressed {
public:
BEGIN_COM_MAP(CBeachBallAttitude)
COM_INTERFACE_ENTRY(ITakeUpSpace)
COM_INTERFACE_ENTRY(IWishIWereMoreUseful)
COM_INTERFACE_ENTRY(ITryToBeHelpful)
COM_INTERFACE_ENTRY(IAmDepressed)
END_COM_MAP()
...
};
The following use of this
tear-off implementation compiles and exhibits the appropriate
behavior, but the overhead of even a single tear-off is
exorbitant:
class CBeachBall :
public CComObjectRootEx<CBeachBall>,
public ISphere,
public IRollableObject,
public IPlaything
// No tearoff interfaces in base class list
{
public:
BEGIN_COM_MAP(CBeachBall)
COM_INTERFACE_ENTRY(ISphere)
COM_INTERFACE_ENTRY(IRollableObject)
COM_INTERFACE_ENTRY(IPlaything)
// tearoffs are listed in the interface map
COM_INTERFACE_ENTRY_TEAR_OFF(IID_ILethalObject,
CBeachBallLethalness)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_ITakeUpSpace,
CBeachBallAttitude)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_IWishIWereMoreUseful,
CBeachBallAttitude)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_ITryToBeHelpful,
CBeachBallAttitude)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_IAmDepressed,
CBeachBallAttitude)
END_COM_MAP()
...
};
Because we've grouped the "attitude" interfaces
together into a single tear-off implementation, every time the
client queries for any of them, it pays the overhead of all of
them. To allow this kind of grouping but avoid the overhead of
creating a new instance for every query, ATL provides an
implementation of a cached
tear-off. The owner holds a cached tear-off if there is even
one outstanding interface to the tear-off. The initial query
creates and caches the tear-off. Subsequent queries use the cached
tear-off. The final release deletes the tear-off instance.
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF
To support caching tear-offs, ATL provides
another interface macro:
#define COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk) \
{ &iid, \
(DWORD_PTR)&ATL::_CComCacheData< \
ATL::CComCreator< ATL::CComCachedTearOffObject< x > >, \
(DWORD_PTR)offsetof(_ComMapClass, punk) >::data, \
_Cache },
The _CComCacheData class is used to
stuff a pointer into an _ATL_CACHEDATA structure:
struct _ATL_CACHEDATA {
DWORD dwOffsetVar;
_ATL_CREATORFUNC* pFunc;
};
The use of this structure allows the dw
to point to a Creator function pointer as well as another member,
an offset. The offset is from the base of the owner class to the
member data that is used to cache the pointer to the tear-off. The
_Cache function, another static member function of
CComObjectRootBase, uses the offset to calculate the
address of the pointer and checks the pointer to determine whether
to create a new instance of the cached tear-off:
static HRESULT WINAPI
CComObjectRootBase::_Cache(
void* pv,
REFIID iid,
void** ppvObject,
DWORD_PTR dw)
{
HRESULT hRes = E_NOINTERFACE;
_ATL_CACHEDATA* pcd = (_ATL_CACHEDATA*)dw;
IUnknown** pp = (IUnknown**)((DWORD_PTR)pv + pcd->dwOffsetVar);
if (*pp == NULL) hRes = pcd->pFunc(pv, __uuidof(IUnknown),
(void**)pp);
if (*pp != NULL) hRes = (*pp)->QueryInterface(iid, ppvObject);
return hRes;
}
Just as an instance of a tear-off uses
CComTearOffObject instead of CComObject to
provide the implementation of IUnknown, cached tear-offs
use CComCachedTearOffObject.
CComCachedTearOffObject is nearly identical to
CComAggObject because of the way that the lifetime
and identity of the tear-off are subsumed by that of the owner. The
only difference is that the cached tear-off, like the tear-off,
initializes the m_pOwner member.
Replacing the inefficient use of
COM_INTERFACE_ENTRY_TEAR_OFF with
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF looks like this:
class CBeachBall :
public CComObjectRootEx<CBeachBall>,
public ISphere,
public IRollableObject,
public IPlaything {
public:
BEGIN_COM_MAP(CBeachBall)
COM_INTERFACE_ENTRY(ISphere)
COM_INTERFACE_ENTRY(IRollableObject)
COM_INTERFACE_ENTRY(IPlaything)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_ILethalObject,
CBeachBallLethalness)
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ITakeUpSpace,
CBeachBallAttitude, m_spunkAttitude.p)
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_IWishIWereMoreUseful,
CBeachBallAttitude, m_spunkAttitude.p)
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ITryToBeHelpful,
CBeachBallAttitude, m_spunkAttitude.p)
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_IAmDepressed,
CBeachBallAttitude, m_spunkAttitude.p)
END_COM_MAP()
DECLARE_GET_CONTROLLING_UNKNOWN() // See the Aggregation section
...
public:
CComPtr<IUnknown> m_spunkAttitude;
};
Another Use for
Cached Tear-Offs
Cached tear-offs have another use that is in
direct opposition to standard tear-offs: caching per-interface
resources. For example, imagine a dictionary object that implements
a rarely used IHyphenation interface:
interface IHyphenation : public IUnknown {
HRESULT Hyphenate([in] BSTR bstrUnhyphed,
[out, retval] BSTR* pbstrHyphed);
};
Performing hyphenation is a matter of consulting
a giant look-up table. If a CDictionary object were to
implement the IHyphenation interface, it would likely do
so as a cached tear-off to manage the
resources associated with the look-up table. When the hyphenation
cached tear-off is first created, it acquires the look-up table.
Because the tear-off is cached, subsequent queries use the same
look-up table. After all references to the IHyphenation
interface are released, the look-up table can be released. If we
had used a standard tear-off for this same functionality, a naïve
implementation would have acquired the resources for the look-up
table for each tear-off.
|