The Active Template Library Makes Building Compact COM Objects a Joy

Don Box

Don Box is a cofounder of DevelopMentor and wrote the upcoming Creating Components using DCOM and C++ for Addison-Wesley. Don can be reached at dbox@develop.com or at http://www.develop.com/dbox.

I love COM. COM is good. Like a fine pilsner or ale, COM never disappoints. In fact, the more I look at COM, the more I like it. Unfortunately, I work in C++, and C++ does not share my appreciation for the finer things in life.

C++ neither loves nor hates COM. At best, C++ peacefully coexists with COM, allowing me to write the same 13 lines of code that implement IUnknown in each object over and over again. While I'm certain that future C++ compilers and linkers will implement a natural, automatic mapping between C++ objects and COM objects, at present this environment exists only in a laboratory, and it certainly isn't a product that you or I can buy today. For now, the closest one can get is the Active Template Library (ATL).

Why ATL?

The Active Template Library was designed from the ground up to make developing COM objects in C++ easy and flexible. ATL is fairly minimal, which is its greatest strength. (The original version of the ATL shipped as four C++ header files, one of which was empty!) Using ATL, you can build fairly small, self-contained binaries without requiring any additional runtime DLLs.

ATL is representative of the movement away from monolithic, single-tier applications and serves as a good substrate for developing the lightweight COM components required for modern distributed applications. ATL is less of a massive, MFC-like infrastructure than it is a modular, time-saving library that keeps thousands of programmers from implementing IUnknown and IClassFactory over and over again.

ATL does not try to be all things to all people. Version 1 provides very reasonable support for implementing IUnknown, IClassFactory, IDispatch, IConnectionPointContainer, and COM enumeration. Version 2 offers enhanced versions of the original ATL classes in addition to support for writing ActiveX™ controls. ATL does not provide collections and strings (ATL assumes you will use the Standard C++ Library classes for these); ODBC support (the world is moving to COM-based data access that doesn't need wrapping); WinSock wrappers (sockets are so five-minutes-ago); or a complete wrapper for the Win32® API (ATL 2.0's control implementation provides support for implementing dialogs and WndProcs). Also missing from ATL is MFC's document/view model. Instead, ATL assumes that you will use the more scalable and flexible COM approach of connectable outbound COM interfaces (ActiveX controls, for instance) to notify the UI-based objects.

The key idea is to use the right tool for the job. If you are building nonvisual COM components, then ATL is likely to be a much better choice than MFC in terms of development effort, scalability, runtime performance, and executable size. For modern user interfaces based on ActiveX controls, ATL also produces smaller and faster code than MFC. On the other hand, ATL requires more COM knowledge than is needed to operate MFC's Class Wizard. For building double-clickable single-tier applications, ATL is currently no more helpful than the Standard Template Library (STL). For this, MFC remains the superior choice.

The design of ATL is highly inspired by STL, which has become part of the Standard C++ Library included with all ANSI/ISO-compliant C++ compilers. Like STL, ATL uses C++ templates aggressively. Templates are one of the more controversial features in C++. When abused, templates can lead to bloated executables, poor performance, and unintelligible code. Used judiciously, templates provide a degree of generality combined with type-safety that is impossible to achieve otherwise. Like STL, ATL falls between these two extremes. Fortunately, compiler and linker technology is advancing at about the same pace as aggressive template usage, making STL and ATL reasonable choices for current and future development.

Despite its extensive use of templates internally, you can use ATL without ever having to type an angle bracket. This is because ATL ships with the ATL Object Wizard (see Figure 1), which generates a variety of default object implementations based on ATL's template classes. The list of default object types is shown in Figure 2. The ATL Object Wizard allows anyone to get a COM object up and running in a matter of minutes without thinking about COM or ATL. Of course, to take full advantage of ATL you do need to understand C++, templates, and COM programming techniques. However, for a large class of objects, the ATL Object Wizard's default implementations require only the addition of method implementations for any custom interfaces being exported, which for most developers is the whole point of implementing a COM object in the first place.

Figure 1 ATL Object Wizard

Figure 2 ATL Object Wizard Types

Object Type

Supported Interfaces

Notes

Simple Object

None

Add-in Object

IDSAddIn

Maintains a pointer to Developer Studio's IApplication interface

Internet Explorer Object

IObjectWithSite

Maintains a pointer to a site

ActiveX Server Component

None

Supports OnStartPage/OnEndPage and maintains pointers to

ASP-supplied interfaces

Microsoft Transaction Server Object

IObjectControl (opt)

Maintains a pointer to a Microsoft Transaction Server object context

Component Registrar Object

IComponentRegistrar

Supports registering all CLSIDs in a module

Internet Explorer Control

IViewObject

Maintains pointers to site's IOleInPlaceSiteWindowless,

IViewObject2

IOleClientSite, and IAdviseSink interfaces

IViewObjectEx

IOleWindow

IOleInPlaceObject

IOleInPlaceObjectWindowless

IOleInPlaceActiveObject

IOleControl

IOleObject

IPersistStreamInit

Full Control

All Internet Explorer control

Maintains pointers to a site's IOleInPlaceSiteWindowless,

interfaces plus

IOleClientSite, and IAdviseSink interfaces

IQuickActivate

IPersistStorage

ISpecifyPropertyPages

IDataObject

IProvideClassInfo

IProvideClassInfo2

Property Page

IPropertyPage


At first glance, the ATL architecture may seem quite bizarre and arcane. Figure 3 shows a minimal ATL-based inprocess server and its corresponding raw COM counterpart. Refer back to this code as you read about the twists and turns your code must go through prior to actually becoming a COM component. For mainstream component developers just getting up to speed on COM, the actual ATL architecture is irrelevant because the wizards generate perfectly reasonable skeletons that only require method definitions. For serious COM developers and systems programmers, ATL provides an elegant, extensible architecture for building COM components in C++. Once you understand the architecture and move beyond the wizards, ATL can be as expressive and powerful as raw COM programming.

Figure 3 Hello

HelloATL.cpp

////////////////////////////////////////////////////
//
// HelloATL.cpp - 1997, Don Box
//
// An ATL-based implementation of a COM in-process server
//

#include <windows.h>
#include "hello.h"
#define IID_DEFINED
#include "hello_i.c"

#include <atlbase.h>
extern CComModule _Module;
#include <atlcom.h>
#include <atlimpl.cpp>

class ATL_NO_VTABLE CHelloATL 
    :   public CComObjectRootEx<CComMultiThreadModel>,
        public CComCoClass<CHelloATL, &CLSID_HelloATL>,
        public IHello
{
public:
BEGIN_COM_MAP(CHelloATL)
    COM_INTERFACE_ENTRY(IHello)
END_COM_MAP()

    DECLARE_REGISTRY_RESOURCEID(1)

    STDMETHODIMP Hello(BSTR bstr)
    {
        MessageBoxW(0, bstr ? bstr : OLESTR(""), L"Hello!", MB_SETFOREGROUND);
        return S_OK;
    }
};

CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
    OBJECT_ENTRY(CLSID_HelloATL, CHelloATL)
END_OBJECT_MAP()

BOOL WINAPI DllMain(HINSTANCE h, DWORD dwReason, void *)
{
    if (dwReason == DLL_PROCESS_ATTACH)
        _Module.Init(ObjectMap, h);
    else if (dwReason == DLL_PROCESS_DETACH)
        _Module.Term();
    return TRUE;
}

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv)
{ return _Module.GetClassObject(rclsid, riid, ppv); }

STDAPI DllCanUnloadNow(void)
{ return _Module.GetLockCount() ? S_FALSE : S_OK; }

STDAPI DllRegisterServer(void)
{ return _Module.RegisterServer(TRUE); }

STDAPI DllUnregisterServer(void)
{ return _Module.UnregisterServer(); }

HelloATL.rc

1   TYPELIB "hello.tlb"
1   REGISTRY "HelloATL.rgs"

HelloATL.rgs

HKCR {
    NoRemove CLSID {
        ForceRemove {D50841E2-9AAA-11d0-8C20-0080C73925BA} = s 'HelloATL' {
            InprocServer32 = s '%MODULE%' {
                val ThreadingModel = s 'Both'
            }
        }
    }
}

HelloSDK.cpp

////////////////////////////////////////////////////
//
// HelloSDK.cpp - 1997, Don Box
//
// An SDK-based implementation of a COM in-process server
//

#include <windows.h>
#include "hello.h"
#define IID_DEFINED
#include "hello_i.c"

LONG g_cLocks = 0;
inline LONG LockModule(void) { return InterlockedIncrement(&g_cLocks); }
inline LONG UnlockModule(void) { return InterlockedDecrement(&g_cLocks); }

class CHelloSDK : public IHello {
    LONG m_dwRef;
public:
    CHelloSDK(void) : m_dwRef(0) { LockModule(); }
    virtual ~CHelloSDK(void) { UnlockModule(); }

    STDMETHODIMP QueryInterface(REFIID riid, void **ppv) {
        if (riid == IID_IUnknown || riid == IID_IHello)
            *ppv = (IHello*)this;
        else
            return (*ppv = 0), E_NOINTERFACE;
        ((IUnknown*)*ppv)->AddRef();
        return S_OK;
    }

    STDMETHODIMP_(ULONG) AddRef(void) 
    { return InterlockedIncrement(&m_dwRef); }

    STDMETHODIMP_(ULONG) Release(void) { 
        LONG res = InterlockedIncrement(&m_dwRef); 
        if (res == 0)
            delete this;
        return res;
    }

    STDMETHODIMP Hello(BSTR bstr) {
        MessageBoxW(0, bstr ? bstr : OLESTR(""), L"Hello!", MB_SETFOREGROUND);
        return S_OK;
    }
};

HINSTANCE g_hInstance = 0;

BOOL WINAPI DllMain(HINSTANCE h, DWORD dwReason, void *) {
    if (dwReason == DLL_PROCESS_ATTACH)
        g_hInstance = h;
    return TRUE;
}

class CHelloClassObject : public IClassFactory {
public:
    STDMETHODIMP QueryInterface(REFIID riid, void **ppv) {
        if (riid == IID_IUnknown || riid == IID_IClassFactory)
            *ppv = (IClassFactory *)this;
        else
            return (*ppv = 0), E_NOINTERFACE;
        ((IUnknown*)*ppv)->AddRef();
        return S_OK;
    }

    STDMETHODIMP_(ULONG) AddRef(void) { return LockModule(); }
    STDMETHODIMP_(ULONG) Release(void) { return UnlockModule(); }

   
    STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) {
        *ppv = 0;
        if (pUnkOuter) return CLASS_E_NOAGGREGATION;
        CHelloSDK *p = new CHelloSDK;
        if (!p) return E_OUTOFMEMORY;
        p->AddRef();
        HRESULT hr = p->QueryInterface(riid, ppv);
        p->Release();
        return hr;
    }

    STDMETHODIMP LockServer(BOOL b) 
    { return (b ? LockModule() : UnlockModule()), S_OK; }
};

CHelloClassObject g_classObject;

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv) { 
    if (rclsid == CLSID_HelloSDK)
        return g_classObject.QueryInterface(riid, ppv);
    return (*ppv = 0), CLASS_E_CLASSNOTAVAILABLE;
}

STDAPI DllCanUnloadNow(void)
{ return g_cLocks ? S_FALSE : S_OK; }

STDAPI DllRegisterServer(void) { 
    char szFileName[MAX_PATH]; OLECHAR wszFileName[MAX_PATH];
    GetModuleFileName(g_hInstance, szFileName, MAX_PATH);
    mbstowcs(wszFileName, szFileName, MAX_PATH);

    ITypeLib *ptl = 0;
    HRESULT hr = LoadTypeLib(wszFileName, &ptl);
    if (FAILED(hr)) return hr;
    hr = RegisterTypeLib(ptl, wszFileName, 0);
    ptl->Release();
    if (FAILED(hr)) return hr;

    LONG err = RegSetValueA(HKEY_CLASSES_ROOT, 
                            "CLSID\\{D50841E3-9AAA-11d0-8C20-0080C73925BA}",
                            REG_SZ, "HelloSDK", 9);
    if (err != ERROR_SUCCESS) goto error_exit;

    HKEY hkey;
    err = RegCreateKeyA(HKEY_CLASSES_ROOT, 
                        "CLSID\\{D50841E3-9AAA-11d0-8C20-0080C73925BA}"
                        "\\InprocServer32", &hkey);
    if (err != ERROR_SUCCESS) goto error_exit;

    err = RegSetValueExA(hkey, 0, 0, REG_SZ, (BYTE*)szFileName, 
                         strlen(szFileName) + 1);
    if (err != ERROR_SUCCESS) 
        err = RegSetValueExA(hkey,"ThreadingModel",0,REG_SZ, (BYTE*)"Both", 5);
    RegCloseKey(hkey);
error_exit:
    return (err == ERROR_SUCCESS) ? S_OK :
            MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, err);
}

STDAPI DllUnregisterServer(void) { 
    LONG err = RegDeleteKeyA(HKEY_CLASSES_ROOT, 
                             "CLSID\\{D50841E3-9AAA-11d0-8C20-0080C73925BA}"
                             "\\InprocServer32");
    if (err != ERROR_SUCCESS) goto error_exit;
    
    err = RegDeleteKeyA(HKEY_CLASSES_ROOT, 
                        "CLSID\\{D50841E3-9AAA-11d0-8C20-0080C73925BA}");
error_exit:
    return (err == ERROR_SUCCESS) ? S_OK :
            MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, err);
}

HelloSDK.rc

1   TYPELIB "Hello.tlb"

One compelling factor that may sway both casual and serious COM developers to start using ATL is the high degree of ATL support in the Visual C++® 5.0 Integrated Development Environment (IDE). The Visual C++ 5.0 integrated IDL/C++ editor requires that ATL be used.

Getting Started

The easiest way to get a handle on ATL is to look at its support for client-side programming. One of the more problematic issues for novice COM programmers is correctly managing reference counts on interface pointers. There is no runtime enforcement of the COM reference counting laws, which means each client has to ensure that the promises made to the object are kept.

Veteran COM programmers are used to a standard pattern documented in Kraig Brockschmidt's Inside OLE (Microsoft Press). You call a function or method that returns an interface pointer, use the interface pointer for some scope of time, and then you release it. Here's what the pattern looks like in source code:

void f(void) {
  IUnknown *pUnk = 0;
// call 
  HRESULT hr = GetSomeObject(&pUnk);
  if (SUCCEEDED(hr)) {
// use
    UseSomeObject(pUnk);
// release
    pUnk->Release();
                     }
                   }

This pattern is so ingrained in every COM programmer's soul that they often do not write the statements that actually use the pointer until after typing the Release statement at the end of the block. This is much like a C programmer reflexively typing the break immediately after the case statement in a C switch clause.

Remembering to call Release is really not a terrible burden. However, putting the responsibility on the client programmer has two fairly serious problems. The first problem is related to acquiring multiple interface pointers. If a function needs to acquire three interface pointers before doing any actual work, that means three call statements before the first use statement. In source form, this usually means the programmer needs to write a lot of nested conditional statements:

void f(void) {
  IUnknown *rgpUnk[3];
  HRESULT hr = GetObject(rgpUnk);
  if (SUCCEEDED(hr)) {
    hr = GetObject(rgpUnk + 1);
    if (SUCCEEDED(hr)) {
      hr = GetObject(rgpUnk + 2);
      if (SUCCEEDED(hr)) {
         UseObjects(rgpUnk[0], rgpUnk[1],
                    rgpUnk[2]);
         rgpUnk[2]->Release();
      }
      rgpUnk[1]->Release();
    }
    rgpUnk[0]->Release();
  }
}

Code like this often motivates programmers to set their tab stops to one or two spaces or petition management for 21-inch monitors.

When management refuses to provide the proper hardware support for COM programming and starts making noises about the company-wide standard for tab stops, programmers often resort to using the highly controversial yet helpful goto statement:

void f(void) {
  IUnknown *rgpUnk[3];
  ZeroMemory(rgpUnk, sizeof(rgpUnk));
  if (FAILED(GetObject(rgpUnk))) 
    goto cleanup;
  if (FAILED(GetObject(rgpUnk+1))) 
    goto cleanup;
  if (FAILED(GetObject(rgpUnk+2))) 
    goto cleanup;

  UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);

cleanup:
  if (rgpUnk[0]) rgpUnk[0]->Release();
  if (rgpUnk[1]) rgpUnk[1]->Release();
  if (rgpUnk[2]) rgpUnk[2]->Release();
}

While somewhat inelegant, this code at least reduces the need for horizontal scrolling.

The more insidious problem with these two code fragments is related to C++ exceptions. If the function UseObjects throws an exception, the code for releasing the interface pointers is bypassed completely. One option would be to use Win32 Structured Exception Handling (SEH) termination handlers:

void f(void) {
  IUnknown *rgpUnk[3];
  ZeroMemory(rgpUnk, sizeof(rgpUnk));
  __try {
   if (FAILED(GetObject(rgpUnk))) leave;
   if (FAILED(GetObject(rgpUnk+1))) leave;
   if (FAILED(GetObject(rgpUnk+2))) leave;

   UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
  } __finally {
   if (rgpUnk[0]) rgpUnk[0]->Release();
   if (rgpUnk[1]) rgpUnk[1]->Release();
   if (rgpUnk[2]) rgpUnk[2]->Release();
}

Unfortunately, Win32 SEH does not work well in C++. A better approach is to use the intrinsic C++ exception handling model and stop using raw pointers altogether. The Standard C++ Library contains a class, auto_ptr, that is hardcoded to call delete on a pointer in its destructor (which is guaranteed to execute even in the face of exceptions). Analogously, ATL contains a COM-smart pointer, CComPtr, that correctly calls Release in its destructor.

The CComPtr class implements the client side of the basic COM reference counting model and is shown in Figure 4. CComPtr has one data member, which is the raw COM interface pointer. The type of this pointer is passed as a template parameter:

CComPtr<IUnknown> unk;
CComPtr<IClassFactory> cf;

The default constructor initializes the raw pointer data member to null. The smart pointer also has constructors that take either raw pointers or smart pointers of the same type as arguments. In both cases, the smart pointer calls AddRef to manage the reference. CComPtr's assignment operator works with either raw pointers or smart pointers, and automatically releases the held pointer prior to calling AddRef on the newly assigned pointer. Most importantly, the destructor for CComPtr releases the held interface if it is non-null.

Figure 4 CComPtr

template <class T>
class CComPtr {
public:
        typedef T _PtrClass;
        CComPtr() {p=NULL;}
        CComPtr(T* lp) {
                if ((p = lp) != NULL)
                        p->AddRef();
        }
        CComPtr(const CComPtr<T>& lp) {
                if ((p = lp.p) != NULL)
                        p->AddRef();
        }
        ~CComPtr() {if (p) p->Release();}
        void Release() {if (p) p->Release(); p=NULL;}
        operator T*() {return (T*)p;}
        T& operator*() {_ASSERTE(p!=NULL); return *p; }
        T** operator&() { _ASSERTE(p==NULL); return &p; }
        T* operator->() { _ASSERTE(p!=NULL); return p; }
        T* operator=(T* lp){return (T*)AtlComPtrAssign((IUnknown**)&p, lp);}
        T* operator=(const CComPtr<T>& lp) {
                return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p);
        }
        bool operator!(){return (p == NULL);}
        T* p;
};

template <class T, const IID* piid>
class CComQIPtr
{
public:
        typedef T _PtrClass;
        CComQIPtr() {p=NULL;}
    CComQIPtr(T* lp) {
                if ((p = lp) != NULL)
                        p->AddRef();
        }
        CComQIPtr(const CComQIPtr<T,piid>& lp) {
                if ((p = lp.p) != NULL)
                        p->AddRef();
        }
        CComQIPtr(IUnknown* lp)        {
                p=NULL;
                if (lp != NULL)
                        lp->QueryInterface(*piid, (void **)&p);
        }
        ~CComQIPtr() {if (p) p->Release();}
        void Release() {if (p) p->Release(); p=NULL;}
        operator T*() {return p;}
        T& operator*() {_ASSERTE(p!=NULL); return *p; }
        T** operator&() { _ASSERTE(p==NULL); return &p; }
        T* operator->() {_ASSERTE(p!=NULL); return p; }
        T* operator=(T* lp){return (T*)AtlComPtrAssign((IUnknown**)&p, lp);}
    T* operator=(const CComQIPtr<T,piid>& lp) {
                return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p);
        }
        T* operator=(IUnknown* lp) {
                return (T*)AtlComQIPtrAssign((IUnknown**)&p, lp, *piid);
        }
        bool operator!(){return (p == NULL);}
        T* p;
};

The following code demonstrates CComPtr:

void f(IUnknown *pUnk1, IUnknown *pUnk2) {
// ctor calls addref on pUnk1 if non-null
   CComPtr<IUnknown> unk1(pUnk1);  

// ctor calls addref on unk1.p if non-null
  CComPtr<IUnknown> unk2 = unk1; 

// operator = calls release on unk1.p if non-null
// and calls AddRef on unk2.p if non-null
  unk1 = unk2; 

// destructor releases unk1 and unk2 if non-null
} 

Besides correctly implementing the AddRef and Release rules of COM, CComPtr allows the operators shown in Figure 5 to achieve raw and smart-pointer transparency. This means that the following code works the way you might expect:

void f(IUnknown *pUnkCO) {
  CComPtr<IClassFactory> cf;
  HRESULT hr;
// uses operator & to get access &cf.p
  hr = pUnkCO->QueryInterface(IID_IClassFactory,
                              (void**)&cf);

  if (FAILED(hr)) throw hr;
  CComPtr<IUnknown> unk;
// operator-> gets access to cf.p
// operator & gets access to &unk.p
  hr = cf->CreateInstance(0, IID_IUnknown, 
                          (void**)&unk);
  if (FAILED(hr)) throw hr;
// operator IUnknown * returns unk.p
    UseObject(unk);
// destructors release unk.p and cf.p
}

Figure 5 COM Operators

Operator

Description

operator &

Returns address of raw pointer

operator *

Returns dereferenced pointer

operator T *

Returns raw pointer

operator ->

Returns raw pointer

operator !

Used to test nullness

operator bool

Used to test nullness


Except for the lack of explicit Release calls, this code looks like plain vanilla COM code. Armed with the CComPtr class, the troublesome presmart pointer example becomes simple:

void f(void) {
  CComPtr<IUnknown> rgpUnk[3];
  if (FAILED(GetObject(&rgpUnk[0]))) return;
  if (FAILED(GetObject(&rgpUnk[1]))) return;
  if (FAILED(GetObject(&rgpUnk[2]))) return;
  UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
}

Thanks to CComPtr's extensive use of operator overloading, the code compiles and runs without a flaw.

Given the fact that the template class knows the type of pointer it is managing, you might wonder why the smart pointer cannot automatically call QueryInterface in its assignment operator or constructor, in effect completely wrapping IUnknown. Prior to Visual C++ 5.0, there was no way to associate the GUID of an interface with its native C++ type. (Visual C++ 5.0 uses a proprietary declspec to bind an IID to an interface definition.) Since ATL is meant to work with a variety of different C++ compilers, it needs some compiler-independent means of providing the GUID explicitly. Enter the CComQIPtr class.

CComQIPtr is closely related to CComPtr (in fact, it simply adds two member functions). CComQIPtr requires two template arguments: the type of pointer to be managed, and a pointer to the GUID that corresponds to the pointer type. For example, the following code would declare smart pointers to manage IDataObject and IPersist interfaces:

CComQIPtr<IDataObject, &IID_IDataObject> do;
CComQIPtr<IPersist, &IID_IPersist> p;

The advantage of CComQIPtr is that it has overloaded constructors and assignment operators. The homogeneous versions (for example, the versions that accept the same type of interface) simply AddRef the right-hand side of the assignment/initialization. This is exactly how CComPtr works. The heterogeneous versions (the versions that accept type-incompatible interfaces) correctly call QueryInterface to determine if the object actually supports the interface in question:

void f(IPersist *pPersist) {
  CComQIPtr<IPersist, &IID_IPersist> p;
// homogeneous assignment – AddRef's
  p = pPersist;
  CComQIPtr<IDataObject, &IID_IDataObject> do;
// heterogeneous assignment – QueryInterface's
  do = pPersist;
}

In the second assignment statement, since pPersist is not of type IDataObject * but is an IUnknown-derived interface pointer, CComQIPtr calls QueryInterface through pPersist to try to acquire an IDataObject interface pointer to the object. If the QueryInterface succeeds, then the smart pointer will contain the resulting raw IDataObject pointer. If the call to QueryInterface fails, then do.p will be set to null. If the HRESULT value returned by QueryInterface is important, then you must call QueryInterface explicitly as there is no way to retrieve its value from the assignment operator.

Given the utility of CComQIPtr, you might wonder why CComPtr even exists. There are several reasons. First, the initial release of ATL only supported CComPtr, so it must remain around for legacy reasons. Second (and most important), it is illegal to use CComQIPtr with IUnknown because of the overloaded constructor and assignment operator. Since all COM interfaces are by definition type-compatible with IUnknown,

CComPtr<IUnknown> unk;

is functionally equivalent to

CComQIPtr<IUnknown, &IID_IUnknown> unk;

The former works. The latter does not. If you forget, your C++ compiler will remind you.

One other possible reason for preferring the simpler CComPtr is that some developers believe having QueryInterface called silently, without warning, weakens the type system of C++. After all, C++ does not allow you to assign type-incompatible raw pointers without some sort of cast, so why should a smart pointer? Fortunately, developers can choose the type of pointer that best meets their needs.

Many developers look at smart pointers as a way to simplify what otherwise might seem an overly complex programming task. This was my initial instinct as well. However, after observing developers using COM smart pointers and the assumptions that the programmers make, I have come to realize that they introduce as many potential hazards as they solve.

Case in point: assume that I take an existing function based on raw pointers

void f(void) {
  IFoo *pFoo = 0;
  HRESULT hr = GetSomeObject(&pFoo);
  if (SUCCEEDED(hr)) {
    UseSomeObject(pFoo);
    pFoo->Release();
  }
}

and naïvely convert it to use CComPtr.

void f(void) {
  CComPtr<IFoo> pFoo = 0;
  HRESULT hr = GetSomeObject(&pFoo);
  if (SUCCEEDED(hr)) {
    UseSomeObject(pFoo);
    pFoo->Release();  // hmmm…
  }
}

Note that CComPtr and CComQIPtr expose all of the members of the managed interface, including AddRef and Release. Unfortunately, when the client calls Release through the result of operator ->, the smart pointer is oblivious and calls Release a second time in its destructor. This is obviously wrong, yet the compiler and linker are happy to let you run this code. If you are lucky, the debugger will catch this error quickly.

Another interesting hazard of using ATL's smart pointers is the presence of the type cast operator to provide access to the raw pointer. Providing silent cast operators is a bit controversial. When the ANSI/ISO C++ committee adopted a C++ string class, they explicitly prohibited implicit type conversion. Instead, an explicit member function (c_str) must be used to pass a standard C++ string where a const char * is expected. ATL provides an implicit type conversion operator, favoring convenience over correctness. In general, the conversion operator works in your favor, allowing you to pass smart pointers to functions where raw pointers are expected.

void f(IUnknown *pUnk) { 
  CComPtr<IUnknown> unk = pUnk;
// operator IUnknown *() called implicitly
  CoLockObjectExternal(unk, TRUE, TRUE);
}

This code functions correctly. However, the following code will also compile without even a warning:

HRESULT CFoo::Clone(IUnknown **ppUnk) { 
  CComPtr<IUnknown> unk;
  CoCreateInstance(CLSID_Foo, 0, CLSCTX_ALL,
                   IID_IUnknown, (void **) &unk);
// operator IUnknown *() called implicitly
  *ppUnk = unk;
  return S_OK;
}

In this case, the assignment of the smart pointer (unk) to the raw pointer (*ppUnk) triggered the same cast operator that the previous code fragment used. In the first example, no AddRef is expected. In the second, an AddRef is required.

For more details on the pitfalls of using smart pointers in general, check out Scott Meyer's More Effective C++ (Addison-Wesley, 1995). For more information on COM smart pointers in particular, check out http://www.develop.com/dbox/cxx/SmartPointer.htm, which contains a reprint of an article I wrote on the topic.

Implementing IUnknown

Implementing IUnknown in raw C++ is relatively straightforward. The major differences between implementations of IUnknown typically involve which interfaces will be given out in QueryInterface. Assuming the following interface definitions

interface IMessageSource : IUnknown {
  HRESULT GetNextMessage([out] OLECHAR **ppwsz);
}
interface IPager : IUnknown {
  HRESULT SendMessage([in] const OLECHAR *pwsz);
}
interface IPager2 : IPager {
  HRESULT SendUrgentMessage(void);
}

these C++ class definitions would be a valid implementation of the three interfaces:

class CPager : public IMessageSource, public IPager2 {
  LONG m_dwRef;
public:
  CPager(void) :    
m_dwRef(0) {} virtual ~CPager(void) {} STDMETHODIMP
QueryInterface(REFIID,
void**); STDMETHODIMP_(ULONG)
AddRef(void); STDMETHODIMP_(ULONG)
Release(void); STDMETHODIMP GetNextMessage(OLECHAR **ppwsz); STDMETHODIMP SendMessage(const COLECHAR * pwsz); STDMETHODIMP SendUrgentMessage(void); };

If the object will be heap-based (that is, created internally only via operator new) and will run only in Single Threaded Apartments (STA), the following would be a reasonable implementation of AddRef and Release:

STDMETHODIMP_(ULONG) CPager::AddRef() {
  return ++m_dwRef; 
}
STDMETHODIMP_(ULONG) CPager::Release(){
  ULONG result = —m_dwRef;
  if (result == 0)
    delete this;
  return result;
}

If the object is expected to run in Multithreaded Apartments (MTA), the ++ and - - operators would need to be replaced with calls to the Win32 atomic increment and decrement routines:

STDMETHODIMP_(ULONG) CPager::AddRef() {
  return InterlockedIncrement(&m_dwRef); 
}
STDMETHODIMP_(ULONG) CPager::Release(){
  ULONG result = InterlockedDecrement(&m_dwRef);
  if (result == 0)
    delete this;
  return result;
}

In either type of apartment, the following would be the correct implementation of QueryInterface:

STDMETHODIMP CPager::QueryInterface(REFIID riid, 
                                    void **ppv) {
  if (riid == IID_IUnknown)
    *ppv = (IMessageSource*)this;
  else if (riid == IID_IMessageSource)
    *ppv = (IMessageSource*)this;
  else if (riid == IID_IPager)
    *ppv = (IPager*)this;
  else if (riid == IID_IPager2)
    *ppv = (IPager2*)this;
  else
    return (*ppv = 0), E_NOINTERFACE;
  ((IUnknown*)*ppv)->AddRef();
  return S_OK; 
}

The bottom four lines of QueryInterface tend to be the same for all objects. The remaining lines, however, vary from class to class based on the type hierarchy of the object.

Given the regularity of IUnknown implementations, it's logical that ATL would provide facilities for removing these boilerplate statements from your code. It does this by providing a flexible and extensible class hierarchy that allows developers to specify threading, server locking, and object lifetime behavior simply by specifying the correct set of classes to use.

If you look at the ATL class hierarchy for implementing IUnknown, the first parameterized behavior that you encounter is threading. ATL allows you to build objects and servers that are optimized for STA or MTA usage portably, without changing your source code. By default, ATL projects build objects that are MTA-safe, which requires additional code and state beyond what is needed to build STA-only objects. By defining the appropriate preprocessor symbols, you can change the default threading behavior to support either single STA or multiple STA-based projects.

Compiling your project with

/D _ATL_SINGLE_THREADED

changes the default threading model of your server to support only one STA-based thread. This would be appropriate for out-of-process, STA-based servers that do not create their own threads. When you use this option, all accesses to ATL's global state will be completely concurrent, with no locking. Although this option seems more efficient, it essentially limits your ATL server to only a single thread.

Compiling your project with

/D _ATL_APARTMENT_THREADED

changes the default threading model of your server to support multiple STA-based threads. This option is appropriate for building inprocess servers that support the ThreadingModel=Apartment registry option. It is also required for STA-based out-of-process servers that will be creating additional STA-based threads. Using this option causes ATL to protect its global state by using a critical section to allow thread-safe access.

Compiling your project with

/D _ATL_FREE_THREADED

creates servers that are compatible with any threading environment. This means that ATL's global state will be locked with a critical section, and each object will have its own private critical section to protect its instance state. If none of these symbols is defined, the ATL headers assume _ATL_FREE_THREADED for safety.

To program threading correctly in ATL, you have to understand ATL's notion of a threading model. ATL defines three threading model classes that implement a small number of inline operations that are needed to implement thread-safe behavior. Each of the three threading model classes exposes two static functions, Increment and Decrement. These take a pointer to a long integer as a parameter and perform the fastest increment and decrement operations that are legal for the specified threading model. Each threading model also exposes two nested typedefs, AutoCriticalSection and CriticalSection, which are either wrappers to the Win32 CRITICAL_SECTION API or are empty classes that have compatible signatures but no actual implementation. Again, the actual type of critical section used is based on the specified threading model.

The three threading models implemented by ATL are CComMultiThreadModel, CComSingleThreadModel, and CComMultiThreadModelNoCS. CComMultiThreadModel uses InterlockedIncrement/InterlockedDecrement and real CRITICAL_SECTIONS. CComSingleThreadModel uses the more efficient ++ and - - operators and no-op CRITICAL_SECTIONS. The hybrid CComMultiThreadModelNoCS uses InterlockedIncrement/InterlockedDecrement but no-op CRITICAL_SECTIONS. The third model is useful for objects that live in MTAs but do not require any locking of their data members.

Finally, ATL exposes two typedefs, CComObjectThreadModel and CComGlobalsThreadModel, that are conditionally compiled to yield the most efficient yet safe behavior for objects and global variables, respectively. Depending on which of the three preprocessor symbols are defined, each of these type names aliases one of the three ThreadModel classes described above. Figure 6 shows the mappings based on the preprocessor symbols used by ATL.

Figure 6 ATL TypedefTranslations

ATL Typedef

_ATL_SINGLE_THREADED
Translation

_ATL_APARTMENT_THREADED
Translation

_ATL_FREE_THREADED
Translation

CComGlobalsThreadModel

CComSingleThreadModel

CComMultiThreadModel

CComMultiThreadModel

CComObjectThreadModel

CComSingleThreadModel

CComSingleThreadModel

CComMultiThreadModel


Given just the threading model type hierarchy described above, you could add reasonable parameterized threading behavior to any COM class (see Figure 7). Compiling using the default options (_ATL_FREE_THREADED) adds a real critical section to your object, and the Lock and Unlock methods map to inline calls to the EnterCriticalSection/LeaveCriticalSection API functions. Also, the AddRef and Release methods will use InterlockedIncrement/InterlockedDecrement to safely change the object's reference count.

Figure 7 Parameterized Threading

class CPager : public IPager {
  LONG m_dwRef;
  typedef CComObjectThreadModel _ThreadModel;
  _ThreadModel::CComAutoCriticalSection m_critsec;
    :   :   :   :
  STDMETHODIMP_(ULONG) CPager::AddRef() {
    return _ThreadModel::Increment(&m_dwRef); 
  }
  STDMETHODIMP_(ULONG) CPager::Release(){
    ULONG res = _ThreadModel::Decrement(&m_dwRef);
    if (res == 0)
      delete this;
    return res;
  }
  STDMEHTHODIMP SendUrgentMessage() {
// ensure that we are only thread   
    m_critsec.Lock(); 
// perform work
    this->GenerateMessage();
    this->WakeUpUser();
// allow other threads
    m_critsec.Unlock();
    return S_OK;
  }
};

If the code shown previously was compiled using either the _ATL_APARTMENT_THREADED or _ATL_SINGLE_THREADED options, then the m_critsec data member would be empty, the Lock and Unlock inline routines would become no-ops, and the AddRef and Release methods would use the more efficient ++ and - - operators. If the object was an ActiveX control that would be exposed as a ThreadingModel=Apartment inprocess server, this would yield smaller and faster code that is no less safe or correct.

Occasionally, it's useful to build an object that will run in an MTA (which means that its AddRef and Release must be thread-safe), but requires no additional locking. For this kind of object, the hybrid CComMultiThreadModelNoCS would be appropriate. By changing the class's typedef from the generic

typedef CComObjectThreadModel _ThreadModel;

to the more specific

typedef CComMultiThreadModelNoCS _ThreadModel;

you get a thread-safe AddRef and Release (as the Increment and Decrement methods map to InterlockedIncrement and InterlockedDecrement) without paying for an actual CRITICAL_SECTION per object (CComAutoCriticalSection would map to CComFakeCriticalSection).

Implementing Interfaces

Now that you're armed with a knowledge of ATL's ThreadModel classes, let's look at how ATL implements IUnknown. The most unintuitive (yet powerful) aspect of ATL is that the class you implement is actually abstract and cannot be instantiated directly. You implement a C++ class that derives from a core generic IUnknown implementation. However, the actual mapping of QueryInterface, AddRef, and Release onto real code is deferred until you decide in what context your object will operate. This flexibility lets you implement the core functionality of your object once and then map in support for COM aggregation, tear-offs, heap versus stack allocation, and server locking after the fact. Figure 8 shows the type hierarchy of a typical ATL-based class.

Figure 8 Type Hierarchy of a Typical ATL-based Class

As shown in Figure 9, the core of ATL's IUnknown implementation lies in the CComObjectRootBase and CComObjectRootEx classes. These two classes expose three methods, OuterQueryInterface, OuterAddRef, and OuterRelease, which are used to delegate the IUnknown functionality to an external implementation. These methods are used when implementing COM aggregation and tearoffs. Three other methods—InternalQueryInterface, InternalAddRef, and InternalRelease—implement the native reference counting and interface navigation of the object.

Figure 9 CComObjectRoot

class CComObjectRootBase {
public:
// C++ constuctor
  CComObjectRootBase() { m_dwRef = 0L; }

// ATL psuedo-constructor and and psuedo-destructors
  HRESULT FinalConstruct() { return S_OK; } 
  void FinalRelease() {}

// Inner Unknown function (InternalAddRef/Release supplied by derived class)
  static HRESULT WINAPI InternalQueryInterface(void* pThis, 
            const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) {
    HRESULT hRes = AtlInternalQueryInterface(pThis,pEntries,iid,ppvObject);
    return _ATLDUMPIID(iid, pszClassName, hRes);
  }

// Outer Unknown functions
  ULONG OuterAddRef()     { return m_pOuterUnknown->AddRef(); }
  ULONG OuterRelease() { return m_pOuterUnknown->Release(); }
  HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject) 
  { return m_pOuterUnknown->QueryInterface(iid, ppvObject); }

// ATL creator hook routines
  void SetVoid(void*) {}
  void InternalFinalConstructAddRef() {}
  void InternalFinalConstructRelease() {}

// ATL interface map helper functions
  static HRESULT WINAPI _Break(       void*, REFIID, void**, DWORD);
  static HRESULT WINAPI _NoInterface( void*, REFIID, void**, DWORD);
  static HRESULT WINAPI _Creator(     void*, REFIID, void**, DWORD);
  static HRESULT WINAPI _Delegate(    void*, REFIID, void**, DWORD);
  static HRESULT WINAPI _Chain(       void*, REFIID, void**, DWORD);
  static HRESULT WINAPI _Cache(       void*, REFIID, void**, DWORD);

// The actual reference count OR the back pointer to the real Unknown
  union {
    long m_dwRef;
    IUnknown* m_pOuterUnknown;
  };
};

template <class ThreadModel>
class CComObjectRootEx : public CComObjectRootBase {
public:
  typedef ThreadModel _ThreadModel;
  typedef _ThreadModel::AutoCriticalSection _CritSec;

// Inner Unknown function (InternalQueryInterface supplied by   
CComObjectRootBase) ULONG InternalAddRef() { return _ThreadModel::Increment(&m_dwRef); } ULONG InternalRelease() { return _ThreadModel::Decrement(&m_dwRef); } // Object-level lock operations void Lock() {m_critsec.Lock();} void Unlock() {m_critsec.Unlock();} private: _CritSec m_critsec; };

CComObjectRootEx is a template class that allows you to specify which of the three ATL ThreadModels to use for the class. (If you want to pick up the conditionally compiled default, simply use CComObjectRoot, which is a typedef to CComObjectRootEx<CComObjectThreadModel>.) CComObjectRootEx derives most of its functionality from CComObjectRootBase, which is a fairly thin class containing one data member, which is a union:

union {
  long m_dwRef; 
  IUnknown *m_pOuterUnknown;
};

Depending on how the class is actually used, one or the other union member will be used for the lifetime of a given instance of the class. The m_dwRef member is used in most cases, and m_pOuterUnknown is used to support COM aggregation or tear-offs. CComObjectRootBase exposes OuterQueryInterface, OuterAddRef, and OuterRelease methods that forward IUnknown requests through the m_pOuterUnknown member.

Conversely, CComObjectRootEx exposes the InternalAddRef and InternalRelease methods that actually increment or decrement the m_dwRef variable based on the ThreadModel used as a template parameter. Note that these routines simply increment and decrement the variable but do not actually delete the object. Again, this is because the allocation strategy for the object will be supplied in a derived class that will use these routines to adjust the reference count.

The most interesting aspect of CComObjectRoot hierarchy is its implementation of QueryInterface, which is exposed as the CComObjectRootBase method InternalQueryInterface:

static HRESULT WINAPI 
CComObjectRootBase::InternalQueryInterface(void *pThis, 
                     const _ATL_INTMAP_ENTRY *pEntries, 
                     REFIID riid, void **ppv);

Each class that uses ATL to implement IUnknown must specify an interface map to provide to InternalQueryInterface. An ATL interface map is an array of IID/DWORD/function pointer tuples that indicate what action to take when a given IID is requested via QueryInterface. Each of these tuples is of type _ATL_INTMAP_ENTRY.

struct _ATL_INTMAP_ENTRY {
  const IID* piid;  // the interface id (IID)
  DWORD dw;         // util value
  HRESULT (*pFunc)(void*, REFIID, void**, DWORD); };

The third member of the struct, pFunc, can be interpreted one of three ways. If pFunc is equal to the constant _ATL_SIMPLEMAPENTRY, then the structure member dw is assumed to be an offset into the object and no function needs to be called. In this case, InternalQueryInterface performs the following operation:

(*ppv = LPBYTE(pThis) + pEntries[n].dw)->AddRef();

This offset is usually initialized from the offset of a base interface. If pFunc is non-null but not _ATL_SIMPLEMAPENTRY, then the function it points to will be called, passing the pointer to the object as the first parameter and the utility member dw as the last parameter:

return pEntries[n].pFunc(pThis, riid, ppv, 
                         pEntries[n].dw);

The last entry in the interface map will have a null pFunc value, indicating the end of the map. InternalQueryInterface will return E_NOINTERFACE for any interface not found in the map.

Interface maps are usually composed using ATL's interface map macros. ATL provides 17 different macros that support most of the common techniques used to implement interfaces (multiple inheritance, nested classes, aggregation, and tear-offs). These macros and their raw code equivalents are shown in Figure 10. The following is an example of a class that implements IPager2 and IMessageSource using CComObjectRootEx and interface maps:

class CPager 
 : public IMessageSource, public IPager2,
   public CComObjectRootEx<CComMultiThreadModel>{ 
public:
  CPager(void) {}
  virtual ~CPager(void) {}

BEGIN_COM_MAP(CPager)
  COM_INTERFACE_ENTRY(IMessageSource)
  COM_INTERFACE_ENTRY(IPager2)
  COM_INTERFACE_ENTRY(IPager)
END_COM_MAP()

  STDMETHODIMP GetNextMessage(OLECHAR **ppwsz);
  STDMETHODIMP SendMessage(const COLECHAR * pwsz);
  STDMETHODIMP SendUrgentMessage(void);
};

The previous code generates this simple interface map:

{ &IID_IMessageSource, 0, _ATL_SIMPLEMAPENTRY },
{ &IID_IPager2, 4, _ATL_SIMPLEMAPENTRY },
{ &IID_IPager, 4, _ATL_SIMPLEMAPENTRY},
{ 0, 0, 0 }

When building interface maps, ATL assumes that the first entry in the map will be a simple map entry and uses it to satisfy requests for IID_IUnknown.

Figure 10 ATL Interface Map Macros

Macro

Raw Equivalent

Notes

COM_INTERFACE_ENTRY(X)

if (riid == IID_X)
*ppv = (X*)this;

Normal case for multiple inheritance- based interfaces

COM_INTERFACE_ENTRY2(X, Y)

if (riid == IID_X)
*ppv = (X*)(Y*)this;

Used to resolve intermediate interfaces like IDispatch for dual interfaces

COM_INTERFACE_ENTRY_ BREAK(X)

if (riid == IID_X)
DebugBreak();

Used to trigger the debugger when an interface is requested

COM_INTERFACE_ENTRY_ NOINTERFACE(X)

if (riid == IID_X)
return (*ppv = 0), E_NOINTERFACE;

Used to disable an interface that may be implemented in a base class

COM_INTERFACE_ENTRY_IID(X, Y)

if (riid == X)
*ppv = (Y*)this;

Used to resolve intermediate interfaces like IDispatch for dual interfaces

COM_INTERFACE_ENTRY_IMPL(X)

if (riid == IID_X)
*ppv = (XImpl<ThisClass>*)this;

Used to export ATL-based implementations that do not derive from their interfaces

COM_INTERFACE_ENTRY_IMPL_ IID(X, Y)

if (riid == X)
*ppv = (YImpl<ThisClass>*)this;

Used to export ATL-based implementations that do not derive from their interfaces

COM_INTERFACE_ENTRY2_ IID(iid, X, Y)

if (riid == iid)
*ppv = (X*)(Y*)this;

Used to resolve intermediate interfaces like IDispatch for dual interfaces

COM_INTERFACE_ENTRY_ FUNC(iid,dw, func)

if (riid == iid)
return func(this, riid, ppv, dw);

Used to map an arbitrary function to an IID

COM_INTERFACE_ENTRY_FUNC_ BLIND(dw,func)

if (TRUE)
return func(this, riid, ppv, dw);

Used to map an arbitrary function to a position in the map

COM_INTERFACE_ENTRY_ TEAR_OFF(iid, X)

if (riid == iid)
*ppv = new CComTearOffObject<X>;

Used to create a new tear-off

COM_INTERFACE_ENTRY_ CACHED_TEAR_OFF(iid,X,punk)

if (riid == iid)
{ if (!this->punk)
this->punk = new CComTearOffObject<X>
*ppv = this->punk; }

Used to create a cached tear-off

COM_INTERFACE_ENTRY_ AGGREGATE(iid, punk)

if (riid == iid)
return this->punk->QueryInterface(riid,ppv);

Used to give out an aggregate that is created in a constructor for a given interface

COM_INTERFACE_ENTRY_ AGGREGATE_BLIND(punk)

return this->punk-> QueryInterface(riid,ppv);

Used to give out an aggregate that is created in a constructor

COM_INTERFACE_ENTRY_ AUTOAGGREGATE(i, punk,clsid)

if (riid == i) {
if (!this->punk)
hr = CoCreateInstance(clsid, this, CLSCTX_ALL,
IID_IUnknown, &this->punk);
if (this->punk)
return this->punk->QueryInterface(riid,ppv);
}

Used to give out an aggregate that is created on demand for a given IID

COM_INTERFACE_ENTRY_ AUTOAGGREGATE_BLIND(punk,clsid)

if (!this->punk)
hr = CoCreateInstance(clsid, this, CLSCTX_ALL,
IID_IUnknown, &this->punk);
if (this->punk)
return this->punk->QueryInterface(riid,ppv);

Used to give out an aggregate that is created on demand

COM_INTERFACE_ENTRY_ CHAIN(basename)

return basename::QueryInterface(riid, ppv);

Used to delegate to a base class's map


In addition to supporting IUnknown, ATL provides default implementations for a number of COM interfaces. ATL uses a simple naming convention for these implementations, with most of them being implemented as template classes that take one template parameter, which is the class actually being implemented.

A simple example of this is the interface IObjectWithSite, an interface used to generically provide an object with a pointer to its activation site. ATL provides a default implementation of this interface called IObjectWithSiteImpl. This class provides an IObjectWithSite-compliant binary signature and implements all of the IObjectWithSite methods in a reasonable manner. To use ATL's built-in implementations, you simply need to add the implementation (with the appropriate template parameter) to the base class list, then add an entry to the interface map to expose the implementation's interface via QueryInterface.

For example, to use ATL's IObjectWithSite implementation, do the following:

class CPager 
 : public CComObjectRootEx<CComMultiThreadModel>,
   public IPager,
   public IObjectWithSiteImpl<CPager>
 {
public:
BEGIN_COM_MAP(CPager)
  COM_INTERFACE_ENTRY(IPager)
  COM_INTERFACE_ENTRY_IMPL(IObjectWithSite)
END_INTERFACE_MAP()
  STDMETHODIMP SendMessage(const COLECHAR * pwsz);
};

Because an ATL built-in implementation class is used, the COM_INTERFACE_ENTRY_IMPL macro is used. One reason for this macro is that many of ATL's default implementations do not derive from the interface they implement. This fact would cause the normal COM_INTERFACE_ENTRY macro to return the incorrect offset. For instance, since CPager does not actually derive from IObjectWithSite, the cast used to calculate the offset would not shear into the object, but would instead use the top of the object.

In the example, IObjectWithSiteImpl does not have a base class. Instead, IObjectWithSiteImpl declares its virtual functions in the same order as IObjectWithSite, producing a fully compliant vtable layout. ATL uses this rather arcane technique because it allows the default implementation to support per-interface reference counting, which can be difficult when you're using normal multiple inheritance techniques.

One commonly used ATL default implementation is IDispatchImpl. This class implements the four IDispatch methods for a dual interface, leaving your class to implement the interesting methods beyond IDispatch::Invoke. Unlike most other ATL implementations, this class actually derives from a COM interface and takes several template parameters:

template <
  class T,            // the dual interface
  const IID* piid,    // the IID of the dual
  const GUID* plibid, // the containing TypeLib
  WORD wMajor = 1,    // the TypeLib's version
  WORD wMinor = 0,    // the TypeLib's version
  class tihclass = CComTypeInfoHolder
>
class IDispatchImpl : public T { ... };

Assuming two dual interfaces DIPager and DIMessageSource, this class would be used as follows:

class CPager 
: public CComObjectRootEx<CComMultiThreadModel>,
  public IDispatchImpl<DIMessageSource,
           &IID_DIMessageSource, &LIBID_PagerLib>,
  public IDispatchImpl<DIPager,
           &IID_DIPager, &LIBID_PagerLib>
{
public:
BEGIN_COM_MAP(CPager)
  COM_INTERFACE_ENTRY(DIMessageSource)
  COM_INTERFACE_ENTRY(DIPager)
// next entry designates DIPager as [default]
  COM_INTERFACE_ENTRY2(IDispatch, DIPager)
END_INTERFACE_MAP()
  STDMETHODIMP SendMessage(BSTR pwsz);
  STDMETHODIMP GetNextMessage(BSTR *ppwsz);
};

The first version of ATL used the name CComDualImpl, which is now simply a preprocessor alias to IDispatchImpl, allowing version 1.x projects to compile with the version 2 header files.

Stop Me If I'm Too Abstract

One of the least-intuitive aspects of ATL is that the C++ class you define and implement is still an abstract base class. That's right—you went to all the trouble of deciphering ATL's template classes and macros and you still don't have a class that can be instantiated. Even though you may derive from CComObjectRootEx as well as one or more ATL interface implementations, your object technically has not provided an implementation of the three core IUnknown methods, QueryInterface, AddRef, and Release. If you examine existing pre-ATL COM implementations, most, if not all, method implementations don't care about whether the class will be used as a COM aggregate or a tear-off, whether it will be used as a standalone object or as a contained data member, whether it will be implemented as a heap-based object or as a global variable, or whether the existence of your object should or should not keep the server running. To allow maximal flexibility, all of these aspects are specified after the fact via one of ATL's family of ten generic CComObject classes (see Figure 11).

Figure 11 CComObject and Friends

Classname

Locks Server

Delegates IUnknown

Deletes Object

Notes

CComObject

Yes

No

Yes

The normal case

CComObjectCached

Yes (after2nd AddRef)

No

Yes

Used for objects that are held via pointers internally

CComObjectNoLock

No

No

Yes

Used for objects that don't hold the server running

CComObjectGlobal

Yes (after1st AddRef)

No

No

Useful for global variables

CComObjectStack

No

No

No

Useful for stack-based variables that cannot be AddRefed

CComContainedObject

No

Yes

No

Useful for MFC-style nested classes

CComAggObject

Yes

Yes

Yes

Used for aggregate-only implementations

CComPolyObject

Yes

Yes (if aggregated)

Yes

Used for aggregate/nonaggregate implementations

CComTearOffObject

No

Yes (Query- Interface only)

Yes

Used for tear-offs that are created at each request

CComCachedTearOffObject

No

Yes (through2nd IUnknown)

Yes

Used for tear-offs that are created at the first request and cached


Each of the CComObject classes uses derivation to provide the correct implementation of QueryInterface, AddRef, and Release. All CComObject classes take your class name as a template parameter and create a new class that derives from your class, providing the correct semantics for QueryInterface, AddRef, and Release. Of these classes, CComObjectNoLock is the simplest to understand and is shown in Figure 12. It assumes that your object will be allocated on the heap, which means the final call to Release will trigger a call to delete the object. CComObjectNoLock assumes your object is not aggregatable and that the existence of your object should not keep the server running (hence the suffix NoLock).

Figure 12 CComObject

template <class Base>
class CComObjectNoLock : public Base {
public:
        typedef Base _BaseClass;
        CComObjectNoLock(void* = NULL){}
        ~CComObjectNoLock() {m_dwRef = 1L; FinalRelease();}

        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 _InternalQueryInterface(iid, ppvObject);}
};

template <class Base>
class CComObject : public Base {
public:
        typedef Base _BaseClass;
        CComObject(void* = NULL) { _Module.Lock(); }
        ~CComObject() {        m_dwRef = 1L; FinalRelease(); _Module.Unlock();        }

        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 _InternalQueryInterface(iid, ppvObject);}
        static HRESULT WINAPI CreateInstance(CComObject<Base>** pp);
};

To allocate an instance of the ATL-based CPager class on the heap, simply wrap a CComObject template around the class name, as follows:

IPager *p = newCComObjectNoLock<CPager>();

The class CComObjectNoLock<CPager> derives from CPager and adds implementations of QueryInterface, AddRef, and Release that use the InternalQueryInterface, InternalAddRef, and InternalRelease provided by CPager. Because the object will be heap-based, a call to delete will occur in the CComObjectNoLock class's Release implementation when InternalRelease returns zero.

The class CComObject is normally used for heap-based objects that need to hold the server running. Like many of the CComObject family of classes, CComObject assumes the presence of a global variable named _Module that has two methods, Lock and Unlock.These methods are analogous to MFC's AfxOleLockApp and AfxOleUnlockApp routines. The constructor of CComObject calls the _Module's Lock method, and the destructor of CComObject calls the _Module's Unlock method. ATL provides a class, CComModule, that implements these methods in an appropriate manner for inproc servers. When building outofproc servers, a derived class (canonically called CExeModule) must override the default Lock and Unlock in a manner appropriate for shutting down the server. ATL AppWizard-based projects get a free class that uses PostThreadMessage to terminate the main thread's message loop.

Exposing Your Class

Given the implementation of CComObject, you now have enough infrastructure in place to create COM objects using the C++ new operator. This is of little utility, however, because external clients use CoCreateInstance or CoGetClassObject to create instances of the class. This means that you must expose a class object for each externally creatable class. Fortunately, ATL provides default implementations of the IClassFactory and IClassFactory2 interfaces in its CComClassFactory and CComClassFactory2 classes, respectively.

CComClassFactory is not template-driven, but instead holds a function pointer as a data member and uses this function to create an object. ATL provides a family of class templates with a single static method (CreateInstance) called Creators that provides the correct semantics for creating CComObjectRoot-based objects from CComClassFactory. Figure 13 shows the default creator, CComCreator, which makes an instance of the templatized class and uses ATL's standard FinalConstruct sequence to initialize the object.

Figure 13 ATL Creator

template <class T1> class CComCreator {
public:
    static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) {
                HRESULT hRes = E_OUTOFMEMORY;
                T1* p = NULL;
                ATLTRY(p = new T1(pv))
                if (p != NULL) {
                        p->SetVoid(pv);
                        p->InternalFinalConstructAddRef();
                        hRes = p->FinalConstruct();
                        p->InternalFinalConstructRelease();
                        if (hRes == S_OK)
                                hRes = p->QueryInterface(riid, ppv);
                        if (hRes != S_OK)
                                delete p;
                }
                return hRes;
        }
};

template <HRESULT hr> class CComFailCreator {
public:
        static HRESULT WINAPI CreateInstance(void*, REFIID, 
                                             LPVOID*)
    { return hr; }
};

template <class T1, class T2> class CComCreator2 {
public:
        static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, 
                                             LPVOID* ppv) {
                HRESULT hRes = E_OUTOFMEMORY;
                if (pv == NULL)
                        hRes = T1::CreateInstance(NULL, riid, ppv);
                else
                        hRes = T2::CreateInstance(pv, riid, ppv);
                return hRes;
        }
};

ATL relies on two-phase construction largely because it utilizes the Visual C++ 5.0 __declspec(novtable) optimization. This declspec disables vptr initialization in the constructor of an abstract base class, as any vptr that is written in an abstract base will subsequently be overwritten in the derived class. The logic behind this optimization is that it is pointless to initialize a vptr that will never be used. Also, the code size can be reduced because no vtable needs to be allocated for the abstract base class.

Classes that use this technique (and most ATL base classes do) need to be careful not to call virtual functions in their constructors. However, to allow virtual functions to be called at initialization time, ATL Creators call the FinalConstruct method, which is where all nontrivial initialization should take place. In FinalConstruct, your class has been fully constructed from a C++ perspective, which means that all of your object's vptrs have been set to the most-derived type. Also, the CComObject-based wrapper has been constructed as well, allowing you to access your controlling unknown in either COM aggregation or tear-off scenarios.

When stepping through the Creator's call sequence in a debugger, you will notice that the call to InternalFinalConstructAddRef and InternalFinalConstructRelease does nothing in the default case. However, if you plan on creating COM aggregates in your FinalConstruct implementation, you will probably want to raise your object's reference count to one temporarily to prevent premature destruction (this can happen if an aggregate object calls QueryInterface on you). You can protect yourself by adding the following line to your class definition:

DECLARE_PROTECT_FINAL_CONSTRUCT()

This line redefines your class's InternalFinalConstructAddRef and InternalFinalConstructRelease to increment and decrement your reference count, making it safe to pass your pointer to objects that may call QueryInterface.

Each ATL-based project contains an instance of a CComModule-derived class. Aside from implementing the server lifetime behavior mentioned earlier, the CComModule maintains a vector of CLSID-to-ClassObject mappings (called the Object Map) to expose all externally creatable classes. The Object Map is used to implement DllGetClassObject for in-process servers and it provides the arguments for each call to CoRegisterClassObject in out-of-process servers. While I could explicitly use the CComClassFactory and Creator classes directly, they are normally used in the context of ATL's Object Map infrastructure.

An ATL Object Map is an array of _ATL_OBJMAP_ENTRY structures:

struct _ATL_OBJMAP_ENTRY {
  const CLSID* pclsid;
  HRESULT (*pfnUpdateRegistry)(BOOL bRegister);
  HRESULT (*pfnGetClassObject)(void* pv, 
                      REFIID riid, LPVOID* ppv);
  HRESULT (*pfnCreateInstance)(void* pv, 
                      REFIID riid, LPVOID* ppv);
  IUnknown* pCF;
  DWORD dwRegister;
  LPCTSTR  (* pfnGetObjectDescription)(void);
};

The pfnGetClassObject member is called to create a new class object on demand the first time one is needed. The function is passed a Creator function (pfnCreateInstance) as its first argument, and the resulting interface pointer is cached in the pCF member. By creating class objects on demand instead of as static instance variables, no global objects with virtual functions are used, enabling ATL-based projects to be linked without the C runtime library. (The C runtime is required to construct global and static variables prior to DllMain/WinMain.)

Although you can explicitly define the various functions used in the Object Map, the normal technique is to add CComCoClass to the base class list of your class. CComCoClass is a template class that takes two template parameters: the name of your class and a pointer to the corresponding CLSID. It adds the appropriate type definitions and static member functions to provide the functions required by the Object Map. The following code demonstrates using CComCoClass:

class CPager 
 : public CComObjectRootEx<CComMultiThreadModel>,
   public CComCoClass<CPager, &CLSID_Pager>,
   public IPager 
{
public:
BEGIN_COM_MAP(CPager)
  COM_INTERFACE_ENTRY(IPager)
END_INTERFACE_MAP()
  STDMETHODIMP SendMessage(const OLECHAR * pwsz);
};

Once you derive from CComCoClass, your class is ready to be added to the ATL Object Map. ATL provides macros to simplify building an Object Map that are very similar to the interface map macros. The following would build an Object Map for a multi-CLSID server:

BEGIN_OBJECT_MAP(ObjectMap)
  OBJECT_ENTRY(CLSID_Pager, CPager)
  OBJECT_ENTRY(CLSID_Laptop, CLaptop)
END_OBJECT_MAP()

This code builds an array of _ATL_OBJMAP_ENTRYs named ObjectMap, initialized as follows:

static _ATL_OBJMAP_ENTRY ObjectMap[] = {
{  &CLSID_Pager, &CPager::UpdateRegistry,     
   &CPager::_ClassFactoryCreatorClass::CreateInstance, 
   &CPager::_CreatorClass::CreateInstance, NULL, 0, 
   &CPager::GetObjectDescription 
},
{  &CLSID_Laptop, &CLaptop::UpdateRegistry,     
   &CLaptop::_ClassFactoryCreatorClass::CreateInstance, 
   &CLaptop::_CreatorClass::CreateInstance, NULL, 0, 
   &CLaptop::GetObjectDescription 
},
{ 0, 0, 0, 0 } };

The static member functions are defined implicitly by deriving from CComCoClass. The Object Map defined above would normally be passed to ATL using the CComModule's Init method:

_Module.Init(ObjectMap, 
hInstance);

This method is called in either DllMain or WinMain, depending on the type of server being created.

By default, CComCoClass gives your class a standard class factory that allows clients to aggregate your object. You can change the default aggregation behavior by adding one of the following lines to your class definition:

DECLARE_NOT_AGGREGATABLE(CPager)
DECLARE_ONLY_AGGREGATABLE(CPager)
DECLARE_POLY_AGGREGATABLE(CPager)

These macros simply define an ATL Creator as a nested type (_CreatorClass) that will be used to initialize the Object Map. The first two macros are self-explanatory (they prohibit or require aggregation).

The third macro requires some explanation. By default, CComCoClass uses an ATL creator that creates one of two different classes, depending on whether or not aggregation is used. If aggregation is not used, a new instance of CComObject<CPager> will be created. If aggregation is required, then a new instance of CComAggObject<CPager> is created. This means two different vtables must be present in your executable. In contrast, DECLARE_POLY_AGGREGATABLE always creates an instance of CComPolyObject<CPager> and initializes the pointer to the controlling outer appropriately depending on whether or not your object is being aggregated. This means that only one C++ class will be defined, requiring only one vtable. The downside of this technique is that nonaggregated objects must pay the additional 4 bytes per instance for the nondelegating IUnknown pointer. In either case, supporting aggregation requires no real coding, only a simple instance-size versus code-size decision.

ATL and the Registry

The CComModule class exports two methods for self-registration: RegisterServer and UnregisterServer. These methods use the Object Map passed to the Init routine to perform the actual work. As I noted earlier, each Object Map entry contains a pfnUpdateRegistry function pointer that must be provided by the class implementor. The original version of ATL provided routines that would add the standard registry entries for a CLSID automatically, making the default behavior trivial to implement. Unfortunately, these routines were not very extensible and any server that needed something beyond the normal InprocServer32 entries wound up writing registry code by hand.

With the advent of component categories and AppIDs, almost no server can live with the standard registry entries provided by ATL 1.0. In ATL 1.1 or greater, the preferred technique for self-registration is to use Registry Scripts, which are infinitely more flexible. This technique requires a COM implementation of the IRegistrar interface, which can be either statically linked to reduce dependencies or dynamically bound using CoCreateInstance for the smallest code size.

Registry Scripts are simple text files that specify what entries must be added for a given CLSID. Registry Script files have an RGS extension by default, and are added to your executable as custom resources of type REGISTRY. The syntax of registry scripts is fairly straightforward, and can be summarized as follows:

[NoRemove|ForceRemove|val] Name [ = s|d 'Value'] 
{
  ... optional script entries for subkeys
}

The NoRemove prefix indicates that the key should not be removed when unregistering. The ForceRemove prefix indicates that the current key and subkeys should be removed prior to writing the key. The val prefix indicates that the entry is a named value and not a key. The s and d value prefixes indicate REG_SZ or REG_DWORD, respectively. ATL's parser recognizes the standard registry keys HKEY_CLASSES_ROOT and so on, as well as their four character abbreviations (HKCR and so on).

To get a handle on registry scripts, consider the following REGEDIT4 sample:

REGEDIT4
[HKEY_CLASSES_ROOT\CLSID\{XXX}]
@=My Class
[HKEY_CLASSES_ROOT\CLSID\{XXX}\InprocServer32]
@=C:\foo\bar.dll
ThreadingModel=Free

The corresponding registry script would look like this:

HKCR {
  NoRemove CLSID {
    ForceRemove {XXX} = s 'My Class' {
      InprocServer32 = s '%MODULE%' {
        val ThreadingModel = s 'Free'
      }
    }
  }
}

When using resource scripts, your class's UpdateRegistry method can be defined easily using the DECLARE_
REGISTRY_RESOURCEID macro, which takes a resource ID (usually defined in resource.h) as a parameter:

class CPager : public 
CComObjectRoot,public   
IPager
  CComCoClass<CPager,  
              CLSID_Pager> {
  DECLARE_REGISTRY_RESOURCEID(IDR_PAGER)
};

This macro simply defines an UpdateRegistry method that calls the built-in CComModule method UpdateRegistryFromResource. This method invokes the parser on your resource script.

In the registry script shown above, all occurrences of the string %MODULE% will be replaced with the actual results of a call to GetModuleFileName. If you need to add additional registry entries based on dynamic runtime values, you can add additional replacement strings beyond %MODULE% that can be substituted prior to registration. To do this, first pick a new replacement variable to use, using percent signs to delimit the variable name. Here is a sample line from a registry script:

DateInstalled = s '%CURRENTDATE%'

Then, instead of using the DECLARE_REGISTRY_RESOURCEID macro, define a custom UpdateRegistry method. In your method, build a table of replacement name-value pairs that you can feed to the module's registration engine.

Here is an example that substitutes the variable %CURRENTDATE% with a string containing the current date:

static HRESULT WINAPI 
CPager::UpdateRegistry(BOOL b) {
  OLECHAR wsz [1024]; SYSTEMTIME st;  
  GetLocalTime(&st);
  wsprintfW(wsz, L"%d/%d/%d", st.wMonth, st.wDay, 
            st.wYear);
  _ATL_REGMAP_ENTRY rm[] = {
    { OLESTR("CURRENTDATE"), wsz}, { 0, 0 }, 
  };
  return _Module.UpdateRegistryFromResource(IDR_PAGER, 
                                            b, rm);
}

The net effect of this code and registry script is that the registry key DateInstalled will contain the date at the time of install.

Connections

One of the more tedious aspects of COM programming is supporting outbound interfaces using connection points. The design of IConnectionPoint/IConnectionPointContainer may have seemed clever when it was developed, but history has shown it to be less than perfect, both in terms of performance and ease of implementation. ATL provides default implementation classes for each of these interfaces that at least solve the latter problem.

The easiest way to understand how ATL implements connections is to look at an example. Assume the following outbound interface definitions:

interface IPageSink : IUnknown {
  HRESULT OnPageReceived(void);
}
interface IStopSink : IUnknown {
  HRESULT OnShutdown(void);
}

To support these two outbound interfaces, the following ATL code would be sufficient:

class CPager 
: public CComObjectRoot, 
  public CComCoClass<CPager, &CLSID_Pager>,
  public IPager,
  public IConnectionPointContainerImpl<CPager>,
  public IConnectionPointImpl<CPager,&IID_IPageSink>,
  public IConnectionPointImpl<CPager,&IID_IStopSink>
{
BEGIN_COM_MAP(CPager)
  COM_INTERFACE_ENTRY(IPager)
  COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()

BEGIN_CONNECTION_POINT_MAP(CPager)
  CONNECTION_POINT_ENTRY(IID_IPageSink)
  CONNECTION_POINT_ENTRY(IID_IStopSink)
END_CONNECTION_POINT_MAP()

};

The first thing that most experienced COM programmers notice is that the class CPager is deriving from an interface (IConnectionPoint) that it does not expose as part of its COM identity. To support this type of trickery, the ATL class IConnectionPointImpl does not derive from the interface IConnectionPoint. Instead, it defines its virtual functions in the same order as IConnectionPoint.

Secondly, to keep each of these base classes from inheriting the main object's QueryInterface implementation, the first virtual function in IConnectionPointImpl is not QueryInterface. Instead it's a type-compatible method named _LocCPQueryInterface that only catches IID_IConnectionPoint and IID_IUnknown. This trick is fairly esoteric but allows for a completely multiple-inheritance-based implementation.

The implementation of the object's FindConnectionPoint method uses the connection point map defined using ATL's CONNECTION_POINT macros. This map is a table of DWORD offsets into the object that correspond to implementations of IConnectionPoint. FindConnectionPoint simply iterates through the array of offsets, asking each connection point along the way if it is the connection holder for the requested interface.

The example above builds a valid implementation of an object that supports IStopSink and IPageSink as outbound interfaces. However, to make the outbound calls, you need to access the vector of interface pointers managed by the appropriate IConnectionPointImpl class and manually simulate the multicast:

typedef IConnectionPointImpl<CPager, 
                           &IID_IPageSink> base;
for (IUnknown** pp = base::m_vec.begin();
     pp < base::m_vec.end(); 
     pp++)
  if (*pp) 
   ((IPageSink*)(*pp))->OnPageRecieved();

Writing these multicast routines can be quite tedious. Fortunately, ATL provides a Visual Studio™ component, the ATL Proxy Generator (shown in Figure 14), that will read in a type library description of an interface and generate the appropriate IConnectionPointImpl-derived class, adding the appropriate Fire routines for each outbound method. Had the appropriate Connection Point proxies been generated, the class definition would have looked like this:

class CPager 
: public CComObjectRoot, 
  public CComCoClass<CPager, &CLSID_Pager>,
  public IPager,
  public IConnectionPointContainerImpl<CPager>,
  public CProxyIPageSink<CPager >,
  public CProxyIStopSink<CPager >
{
BEGIN_COM_MAP(CPager)
  COM_INTERFACE_ENTRY(IPager)
  COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()

BEGIN_CONNECTION_POINT_MAP(CPager)
  CONNECTION_POINT_ENTRY(IID_IPageSink)
  CONNECTION_POINT_ENTRY(IID_IStopSink)
END_CONNECTION_POINT_MAP()
};

Figure 14 ATL Proxy Generator

To send the outbound method notifications, you would simply call the appropriate Fire_XXX method:

STDMETHODIMP CPager::SendMessage(LPCOLESTR pwsz) {
// send outbound notifications
  HRESULT hr = Fire_OnPageRecieved();
// process normally
  return hr;
}

One limitation of the machine-generated proxy is that it requires a type library definition of the outbound interface. For a large number of COM interfaces, this isn't possible because type libraries loose certain IDL-isms in the translation. For these more complex interfaces, you can start from a machine-generated proxy and modify the generated code to taste.

Is That All?

In this article I gave you an overview of the core architecture of ATL, focusing on the basic programming styles used inside of ATL as well as by ATL users. Virtually all of the aspects of ATL covered in this article were present in ATL 1.1, although some have changed slightly in ATL 2.0. Not covered here are the default implementations of the Active Control interfaces added in ATL 2.0. These interfaces follow the same programming philosophy of ATL 1.1 and are pretty easy to understand once you get used to digesting ATL source code.

When discussing the ATL with colleagues and friends, I find that most people feel very passionately about the design of ATL. Some people find it extremely elegant and beautiful. Others find it extremely twisted and arcane. Many (including myself) feel it is both. Fortunately, ATL programming is very much a "pay as you go" process, so novices can dip their toe into the water without drowning while at the same time authors, columnists, lecturers, and developers get plenty of fodder for another 18 months worth of work.

This article is reproduced from Microsoft Systems Journal. Copyright © 1997 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S. and Canada, or (303) 678-0439 in all other countries. For other inquiries, call (415) 905-2200.