| Tenouk C & C++ | MFC Home | Active Template Library 1 | Active Template Library 3 | Download | Site Index |


 

 

 

 

 

An Introduction To Active Template Library (ATL) Part 2

 

 

 

 

 

 

Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary notes for this tutorial are marshaling and intro to activeX control.

 

  1. Smart Pointers

  2. Giving C++ Pointers Some Brains

  3. Using Smart Pointers

  4. Smart Pointers and COM

  5. ATL's Smart Pointers

  6. CComPtr

  7. Using CComPtr

  8. CComQIPtr

  9. Using CComQIPtr

  10. ATL Smart Pointer Problems

  11. Server-Side ATL Programming

  12. ATL and COM Classes

 

 

Smart Pointers

 

One of the most common uses of templates is for smart pointers. The traditional C++ literature calls C++'s built-in pointers "dumb" pointers. That's not a very nice name, but normal C++ pointers don't do much except point. It's often up to the client to perform details such as pointer initialization.

As an example, let's model two types of software developer using C++ classes. You can start by creating the classes: CVBDeveloper and CCPPDeveloper.

class CVBDeveloper

{

public:

    CVBDeveloper()

    { }

    ~CVBDeveloper()

    { AfxMessageBox("I used VB, so I got home early."); }

    virtual void DoTheWork()

    { AfxMessageBox("Write them forms"); }

};

 

class CCPPDeveloper

{

public:

    CCPPDeveloper()

    {   }

    ~CCPPDeveloper()

    { AfxMessageBox("Stay at work and fix those pointer problems"); }

    virtual void DoTheWork()

    { AfxMessageBox("Hacking C++ code"); }

};

 

The Visual Basic developer and the C++ developer both have functions for eliciting optimal performance. Now imagine some client code that looks like this:

// UseDevelopers.CPP

 

void UseDevelopers()

{

    CVBDeveloper* pVBDeveloper;

    ...

    ...

    ...

    // The VB Developer pointer needs

    //  to be initialized

    //  sometime. But what if

    //  you forget to initialize and later

    //  on do something like this:

    if(pVBDeveloper)

    {

        // Get ready for fireworks

        //  because pVBDeveloper is

        //  NOT NULL, it points

        //  to some random data.

        c->DoTheWork();

    }

}

 

In this case, the client code forgot to initialize the pVBDeveloper pointer to NULL. (Of course, this never happens in real life!) Because pVBDeveloper contains a non-NULL value (the value is actually whatever happened to be on the stack at the time), the test to make sure the pointer is valid succeeds when in fact you're expecting it to fail. The client gleefully proceeds, believing all is well. The client crashes, of course, because the client is "calling into darkness." (Who knows where pVBDeveloper is pointing, probably to nothing that even resembles a Visual Basic developer.) Naturally, you'd like some mechanism for ensuring that the pointers are initialized. This is where smart pointers come in handy.

Now imagine a second scenario. Perhaps you'd like to plug a little extra code into your developer-type classes that performs some sort of operation common to all developers. For example, perhaps you'd like all the developers to do some design work before they begin coding. Consider the earlier VB developer and C++ developer examples. When the client calls DoTheWork(), the developer gets right to coding without proper design, and he or she probably leaves the poor clients in a lurch. What you'd like to do is add a very generic hook to the developer classes so they make sure the design is done before beginning to code. The C++ solution to coping with these problems is called a smart pointer. Let's find out exactly what a smart pointer is.

 

Giving C++ Pointers Some Brains

 

Remember that a smart pointer is a C++ class for wrapping pointers. By wrapping a pointer in a class (and specifically, a template), you can make sure certain operations are taken care of automatically instead of deferring mundane, boilerplate-type operations to the client. One good example of such an operation is to make sure pointers are initialized correctly so that embarrassing crashes due to randomly assigned pointers don't occur.  Another good example is to make certain that boilerplate code is executed before function calls are made through a pointer. Let's invent a smart pointer for the developer model described earlier. Consider a template-based class named SmartDeveloper:

template<class T>

class SmartDeveloper

{

    T* m_pDeveloper;

 

public:

    SmartDeveloper(T* pDeveloper)

    {

        ASSERT(pDeveloper != NULL);

        m_pDeveloper = pDeveloper;

    }

    ~SmartDeveloper()

    { AfxMessageBox("I'm smart so I'll get paid."); }

    SmartDeveloper &operator=(const SmartDeveloper& rDeveloper)

    { return *this; }

    T* operator->() const

    {

        AfxMessageBox("About to de-reference pointer. Make sure everything's okay. ");

        return m_pDeveloper;

    }

};

 

The SmartDeveloper template listed above wraps a pointer, any pointer. Because the SmartDeveloper class is based on a template, it can provide generic functionality regardless of the type associated with the class. Think of templates as compiler-approved macros: declarations of classes (or functions) whose code can apply to any type of data.

We want the smart pointer to handle all developers, including those using VB, Visual C++, Java, and Delphi (among others). The template <class T> statement at the top accomplishes this. The SmartDeveloper template includes a pointer (m_pDeveloper) to the type of developer for which the class will be defined. The SmartDeveloper constructor takes a pointer to that type as a parameter and assigns it to m_pDeveloper. Notice that the constructor generates an assertion if the client passes a NULL parameter to construct SmartDeveloper.

In addition to wrapping a pointer, the SmartDeveloper implements several operators. The most important one is the "->" operator (the member selection operator). This operator is the workhorse of any smart pointer class. Overloading the member selection operator is what turns a regular class into a smart pointer. Normally, using the member selection operator on a regular C++ dumb pointer tells the compiler to select a member belonging to the class or structure being pointed to. By overriding the member selection operator, you provide a way for the client to hook in and call some boilerplate code every time that client calls a method. In the SmartDeveloper example, the smart developer makes sure the work area is in order before working. This example is somewhat contrived. In real life, you might want to put in a debugging hook, for example. Adding the -> operator to the class causes the class to behave like C++'s built-in pointer. To behave like native C++ pointers in other ways, smart pointer classes need to implement the other standard operators such as the de-referencing and assignment operators.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Using Smart Pointers

 

Using smart pointers is really no different from using the regular built-in C++ pointers. Let's start by looking at a client that uses plain vanilla developer classes:

void UseDevelopers()

{

    CVBDeveloper VBDeveloper;

    CCPPDeveloper CPPDeveloper;

 

    VBDeveloper.DoTheWork();

    CPPDeveloper.DoTheWork();

}

No surprises here, executing this code causes the developers simply to come in and do the work. However, you want to use the smart developers, the ones that make sure the design is done before actually starting to hack. Here's the code that wraps the VB developer and C++ developer objects in the smart pointer class:

void UseSmartDevelopers

{

    CVBDeveloper VBDeveloper;

    CCPPDeveloper CPPDeveloper;

 

    SmartDeveloper<CVBDeveloper> smartVBDeveloper(&VBDeveloper);

    SmartDeveloper<CCPPDeveloper> smartCPPDeveloper(&CPPDeveloper);

 

    smartVBDeveloper->DoTheWork();

    smartCPPDeveloper->DoTheWork();

}

Instead of bringing in any old developer to do the work (as in the previous example), the client asks the smart developers to do the work. The smart developers will automatically prepare the design before proceeding with coding.

 

Smart Pointers and COM

 

While the last example was fabricated to make an interesting story, smart pointers do have useful applications in the real world. One of those applications is to make client-side COM programming easier. Smart pointers are frequently used to implement reference counting. Because reference counting is a very generic operation, hoisting client-side reference count management up into a smart pointer makes sense.

Because you're now familiar with the Microsoft Component Object Model, you understand that COM objects expose interfaces. To C++ clients, interfaces are simply pure abstract base classes, and C++ clients treat interfaces more or less like normal C++ objects. However, as you discovered in previous modules, COM objects are a bit different from regular C++ objects. COM objects live at the binary level. As such, they are created and destroyed using language- independent means. COM objects are created via API functions calls. Most COM objects use a reference count to know when to delete themselves from memory. Once a COM object is created, a client object can refer to it in a number of ways by referencing multiple interfaces belonging to the same COM object. In addition, several different clients can talk to a single COM object. In these situations, the COM object must stay alive for as long as it is referred to. Most COM objects destroy themselves when they're no longer referred to by any clients. COM objects use reference counting to accomplish this self-destruction.

To support this reference-counting scheme, COM defines a couple of rules for managing COM interfaces from the client side. The first rule is that creating a new copy of a COM interface should result in bumping the object's reference count up by one. The second rule is that clients should release interface pointers when they have finished with them. Reference counting is one of the more difficult aspects of COM to get right, especially from the client side. Keeping track of COM interface reference counting is a perfect use of smart pointers.

For example, the smart pointer's constructor might take the live interface pointer as an argument and set an internal pointer to the live interface pointer. Then the destructor might call the interface pointer's Release function to release the interface so that the interface pointer will be released automatically when the smart pointer is deleted or falls out of scope. In addition, the smart pointer can help manage COM interfaces that are copied.

For example, imagine you've created a COM object and you're holding on to the interface. Suppose you need to make a copy of the interface pointer (perhaps to pass it as an out parameter). At the native COM level, you'd perform several steps. First you must release the old interface pointer. Next you need to copy the old pointer to the new pointer. Finally you must call AddRef() on the new copy of the interface pointer. These steps need to occur regardless of the interface being used, making this process ideal for boilerplate code. To implement this process in the smart pointer class, all you need to do is override the assignment operator. The client can then assign the old pointer to the new pointer. The smart pointer does all the work of managing the interface pointer, relieving the client of the burden.

 

ATL's Smart Pointers

 

Much of ATL's support for client-side COM development resides in a pair of ATL smart pointers: CComPtr and CComQIPtr. CComPtr is a basic smart pointer that wraps COM interface pointers. CComQIPtr adds a little more smarts by associating a GUID (for use as the interface ID) with a smart pointer. Let's start by looking at CComPtr.

 

CComPtr

 

Here's an abbreviated version of CComPtr showing its most important parts:

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);

    }

    T* p;

};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

CComPtr is a fairly basic smart pointer. Notice the data member p of type T (the type introduced by the template parameter). CComPtr's constructor performs an AddRef() on the pointer while the destructor releases the pointer, no surprises here. CComPtr also has all the necessary operators for wrapping a COM interface. Only the assignment operator deserves special mention. The assignment does a raw pointer re-assignment. The assignment operator calls a function named AtlComPtrAssign():

 

ATLAPI_(IUnknown*) AtlComPtrAssign(IUnknown** pp, IUnknown* lp)

{

    if (lp != NULL)

        lp->AddRef();

    if (*pp)

        (*pp)->Release();

    *pp = lp;

    return lp;

}

 

AtlComPtrAssign() does a blind pointer assignment, AddRef()-ing the assignee before calling Release() on the assignor. You'll soon see a version of this function that calls QueryInterface(). CComPtr's main strength is that it helps you manage the reference count on a pointer to some degree.

 

Using CComPtr

 

In addition to helping you manage AddRef() and Release() operations, CComPtr can help you manage code layout. Looking at a bit of code will help illustrate the usefulness of CComPtr. Imagine that your client code needs three interface pointers to get the work done as shown here:

void GetLottaPointers(LPUNKNOWN pUnk)

{

    HRESULT hr;

    LPPERSIST pPersist;

    LPDISPATCH pDispatch;

    LPDATAOBJECT pDataObject;

    hr = pUnk->QueryInterface(IID_IPersist, (LPVOID *)&pPersist);

    if(SUCCEEDED(hr))

    {

        hr = pUnk->QueryInterface(IID_IDispatch, (LPVOID *)&pDispatch);

        if(SUCCEEDED(hr))

        {

            hr = pUnk->QueryInterface(IID_IDataObject, (LPVOID *) &pDataObject);

            if(SUCCEEDED(hr))

            {

                DoIt(pPersist, pDispatch, pDataObject);

                pDataObject->Release();

            }

            pDispatch->Release();

         }

         pPersist->Release();

    }

}

You could use the controversial goto statement (and risk facing derisive comments from your co-workers) to try to make your code look cleaner, like this:

void GetLottaPointers(LPUNKNOWN pUnk)

{

    HRESULT hr;

    LPPERSIST pPersist; LPDISPATCH pDispatch;

    LPDATAOBJECT pDataObject;

 

    hr = pUnk->QueryInterface(IID_IPersist, (LPVOID *)&pPersist);

    if(FAILED(hr)) goto cleanup;

 

    hr = pUnk->QueryInterface(IID_IDispatch, (LPVOID *) &pDispatch);

    if(FAILED(hr)) goto cleanup;

 

    hr = pUnk->QueryInterface(IID_IDataObject, (LPVOID *) &pDataObject);

    if(FAILED(hr)) goto cleanup;

 

    DoIt(pPersist, pDispatch, pDataObject);

 

cleanup:

    if (pDataObject) pDataObject->Release();

    if (pDispatch) pDispatch->Release();

    if (pPersist) pPersist->Release();

}

That may not be as elegant a solution as you would like. Using CComPtr makes the same code a lot prettier and much easier to read, as shown here:

void GetLottaPointers(LPUNKNOWN pUnk)

{

    HRESULT hr;

    CComPtr<IUnknown> persist;

    CComPtr<IUnknown> dispatch;

    CComPtr<IUnknown> dataobject;

 

    hr = pUnk->QueryInterface(IID_IPersist, (LPVOID *)&persist);

    if(FAILED(hr)) return;

 

    hr = pUnk->QueryInterface(IID_IDispatch, (LPVOID *) &dispatch);

    if(FAILED(hr)) return;

 

    hr = pUnk->QueryInterface(IID_IDataObject, (LPVOID *) &dataobject);

    if(FAILED(hr)) return;

 

    DoIt(pPersist, pDispatch, pDataObject);

 

    // Destructors call release...

}

At this point, you're probably wondering why CComPtr doesn't wrap QueryInterface(). After all, QueryInterface() is a hot spot for reference counting. Adding QueryInterface() support for the smart pointer requires some way of associating a GUID with the smart pointer. CComPtr was introduced in the first version of ATL. Rather than disrupt any existing code base, Microsoft introduced a beefed-up version of CComPtr named CComQIPtr.

 

CComQIPtr

 

Here's part of CComQIPtr's definition:

template <class T, const IID* piid = &__uuidof(T)>

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;

};

What makes CComQIPtr different from CComPtr is the second template parameter, piid, the interfaces's GUID. This smart pointer has several constructors: a default constructor, a copy constructor, a constructor that takes a raw interface pointer of unspecified type, and a constructor that accepts an IUnknown interface as a parameter. Notice in this last constructor that if the developer creates an object of this type and initializes it with a plain old IUnknown pointer, CComQIPtr calls QueryInterface() using the GUID template parameter. Also notice that the assignment to an IUnknown pointer calls AtlComQIPtrAssign() to make the assignment. As you can imagine, AtlComQIPtrAssign() performs a QueryInterface() under the hood using the GUID template parameter.

 

Using CComQIPtr

 

Here's how you might use CComQIPtr in some COM client code:

void GetLottaPointers(ISomeInterface* pSomeInterface)

{

    HRESULT hr;

    CComQIPtr<IPersist, &IID_IPersist> persist;

    CComQIPtr<IDispatch, &IID_IDispatch> dispatch;

    CComPtr<IDataObject, &IID_IDataObject> dataobject;

 

    dispatch = pSomeInterface;   // implicit QI

    persist = pSomeInterface;    //  implicit QI

    dataobject = pSomeInterface; //  implicit QI

 

    DoIt(persist, dispatch, dataobject); // send to a function

                                         // that needs IPersist*,

                                         // IDispatch*, and

                                         // IDataObject*

 

    // Destructors call release...

}

The CComQIPtr is useful whenever you want the Java-style or Visual Basic-style type conversions. Notice that the code listed above didn't require any calls to QueryInterface() or Release(). Those calls happened automatically.

 

ATL Smart Pointer Problems

 

Smart pointers can be quite convenient in some places (as in the CComPtr example where we eliminated the goto statement). Unfortunately, C++ smart pointers aren't the panacea that programmers pray for to solve their reference-counting and pointer-management problems. Smart pointers simply move these problems to a different level. One situation in which to be very careful with smart pointers is when converting from code that is not smart-pointer based to code that uses the ATL smart pointers. The problem is that the ATL smart pointers don't hide the AddRef() and Release() calls. This just means you need to take care to understand how the smart pointer works rather than be careful about how you call AddRef() and Release(). For example, imagine taking this code:

void UseAnInterface()

{

    IDispatch* pDispatch = NULL;

 

    HRESULT hr = GetTheObject(&pDispatch);

    if(SUCCEEDED(hr))

    {

        DWORD dwTICount;

        pDispatch->GetTypeInfoCount(&dwTICount);

        pDispatch->Release();

    }

}

and capriciously converting the code to use a smart pointer like this:

void UseAnInterface()

{

    CComPtr<IDispatch> dispatch = NULL;

 

    HRESULT hr = GetTheObject(&dispatch);

    if(SUCCEEDED(hr))

    {

        DWORD dwTICount;

        dispatch->GetTypeInfoCount(&dwTICount);

        dispatch->Release();

    }

}

 

Because CComPtr and CComQIPtr do not hide calls to AddRef() and Release(), this blind conversion causes a problem when the release is called through the dispatch smart pointer. The IDispatch interface performs its own release, so the code above calls Release() twice, the first time explicitly through the call dispatch->Release() and the second time implicitly at the function's closing curly bracket.

In addition, ATL's smart pointers include the implicit cast operator that allows smart pointers to be assigned to raw pointers. In this case, what's actually happening with the reference count starts to get confusing. The bottom line is that while smart pointers make some aspect of client-side COM development more convenient, they're not foolproof. You still have to have some degree of knowledge about how smart pointers work if you want to use them safely.

 

Server-Side ATL Programming

 

We've covered ATL's client-side support. While a fair amount of ATL is devoted to client-side development aids (such as smart pointers and BSTR wrappers), the bulk of ATL exists to support COM-based servers, which we'll cover next. First you'll get an overview of ATL in order to understand how the pieces fit together. Then you'll re-implement the spaceship example in ATL to investigate ATL's Object Wizard and get a good feel for what it takes to write COM classes using ATL.

 

ATL and COM Classes

 

Your job as a COM class developer is to wire up the function tables to their implementations and to make sure QueryInterface(), AddRef(), and Release() work as advertised. How you get that to happen is your own business. As far as users are concerned, they couldn't care less what methods you use. You've seen two basic approaches so far, the raw C++ method using multiple inheritance of interfaces and the MFC approach using macros and nested classes. The ATL approach to implementing COM classes is somewhat different from either of these approaches.

Compare the raw C++ approach to MFC's approach. Remember that one way of developing COM classes using raw C++ involves multiply inheriting a single C++ class from at least one COM interface and then writing all the code for the C++ class. At that point, you've got to add any extra features (such as supporting IDispatch or COM aggregation) by hand. The MFC approach to COM classes involves using macros that define nested classes (with one nested class implementing each interface). MFC supports IDispatch and COM aggregation, you don't have to do a lot to get those features up and running. However, it's very difficult to paste any new interfaces onto a COM class without a lot of typing. As you saw in Module 23, MFC's COM support uses some lengthy macros. The ATL approach to composing COM classes requires inheriting a C++ class from several template-based classes. However, Microsoft has already done the work of implementing IUnknown for you through the class templates within ATL.

 

 

 

 

 

Further reading and digging:

  1. DCOM at MSDN.

  2. COM+ at MSDN.

  3. COM at MSDN.

  4. Win32 process, thread and synchronization story can be found starting from Module R.

  5. MSDN MFC 7.0 class library online documentation.

  6. MSDN MFC 9.0 class library online documentation - latest version.

  7. MSDN Library

  8. Windows data type.

  9. Win32 programming Tutorial.

  10. The best of C/C++, MFC, Windows and other related books.

  11. Unicode and Multibyte character set: Story and program examples.

 

 

 

 


 

| Tenouk C & C++ | MFC Home | Active Template Library 1 | Active Template Library 3 | Download | Site Index |