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."[6]

[6] As of this writing, Crispin's article is available online at http://msdn.com/library/en-us/dncomg/html/msdn_com_co.asp (http://tinysells.com/50).

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?[7] For example, imagine moving the other rarely used interfaces of CBeachBall to a single tear-off implementation:

[7] The Control interfaces fit into this category for objects that also support nonvisual use.

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[8] 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.

[8] Discussed in Chapter 4, "Objects in ATL."

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.




ATL Internals. Working with ATL 8
ATL Internals: Working with ATL 8 (2nd Edition)
ISBN: 0321159624
EAN: 2147483647
Year: 2004
Pages: 172

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net