Smart Pointers

[Previous] [Next]

One of the most confusing concepts for the developer just getting started with COM is the notion of per-interface reference counting. The idea is really quite simple and can be summed up in two statements:

  • A COM object manages its own lifetime by keeping track of the number of clients that reference it.
  • A COM object must be notified whenever a client reference (that is, an interface pointer) comes into or goes out of existence.

Reference counting is a powerful, elegant concept, but it does add a certain degree of complexity to C++ client code that uses COM objects. The use of CoCreateInstance (or other COM object_creation mechanisms), AddRef, and Release represents a syntactic paradigm shift from the commonly used new and delete keywords that are familiar to C++ developers. In other words, a C++ developer must treat interface pointers differently than other pointers. To illustrate, let's consider the following pseudocode:

 CMyClient::CMyClient() {     m_pBuffer = new CBuffer();     m_pArray = new CArray();     m_pInterface = SomeCreationMechanism(); } CMyClient::~CMyClient() {     delete m_pBuffer;     delete m_pArray;     delete m_pInterface;    // Oops! } 

Obviously, if m_pInterface represents a COM interface pointer, this code will fail miserably. Why? Because the destructor should call m_pInterface->Release instead of trying to free the pointer using the C++ delete keyword. The fact that interface pointers must be handled with different cleanup syntax than other pointers makes it easy for programmers unaccustomed to COM to introduce bugs into their code. This potential hot spot is similar to another common C++ cleanup error. The following code contains a subtle bug:

 CMyCode::CMyCode() {     m_pArray = new CString[256];     m_pBuffer = new TCHAR[256]; } CMyCode::~CMyCode() {     delete m_pArray;    // Oops!     delete m_pBuffer;   // Oops! } 

What's the problem with this code? C++ coding rules specify that when deleting an array of objects, the delete keyword must be followed by a bracket pair to tell the compiler to perform the necessary heap cleanup on each object in the array, like so:

 CMyCode::~CMyCode() {     delete [] m_pArray;    // That's better!     delete [] m_pBuffer;   // This bracket pair is technically                            //  required too! } 

Even though the improper use of the delete keyword (without the necessary brackets) in the cleanup of the m_pBuffer variable doesn't cause a memory leak under Visual C++, it's still technically a bug, albeit a benevolent one. The Visual C++ compiler will clean up the mistake where m_pBuffer is not destroyed using delete []. The improper deletion of m_pArray, an array of CStrings, does result in a memory leak. Cleaning up COM interface pointers is similar to deleting arrays because it also represents a special case, one that you must keep in mind—consciously at first, subconsciously after a bit of practice—when writing code.

Having to remember to call AddRef and Release instead of delete shouldn't be a big deal once you learn the rules, right? Well, theoretically that's true, but C++ exception handling adds a significant degree of complexity to the picture. Recall that when you use exceptions in your code, the C++ compiler will automatically generate the necessary code to unwind the stack back to the catch block. For example, study the following pseudocode:

 try {          IFortuneCookie* pCookie;     CoCreateInstance(clsid, NULL, CLSCTX_ALL,         IID_IFortuneCookie, (void**)&pCookie;     HRESULT hr = pCookie->SetSeed(FALSE);     if(hr != S_OK)         throw hr; } catch(HRESULT hr) {     TCHAR szErrorDesc[256];     ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,          MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),         szErrorDesc, sizeof(szErrorDesc), NULL);     ::MessageBox(NULL, szErrorDesc, "HRESULT Failure", MB_OK);     // Oops! I just leaked pCookie! } 

In this code, the client creates an instance of a COM object and calls one of its methods. Following the rules of proper COM development, the object doesn't throw an exception from within the called method when an error occurs. Rather, it returns an error in HRESULT that the client code throws. The code above attempts to recover elegantly from the error by displaying the text-based description of the error to the user before continuing program execution. The problem with this code is that because pCookie is a dynamically allocated interface pointer rather than a stack variable, the compiler won't release pCookie when the thrown exception causes it to go out of scope. Because pCookie isn't defined in the context of the catch block, the code can't call pCookie->Release. As a result, if the COM object is implemented as an out-of-process server, the server won't shut down when the client code terminates because of the outstanding reference to the object.

In the preceding example, we could easily restructure the code so that the scope of pCookie encompasses the catch block—calling pCookie->Release would then be possible. For typical applications, though, writing code to handle a situation like this one would be challenging if not impossible. To solve this problem, some programming languages—specifically Visual Basic and Java—perform automatic garbage collection and IUnknown encapsulation so that the developer doesn't have to manually code calls to the AddRef, Release, and QueryInterface interface methods. Consider the following Visual Basic code:

 Private Sub Form_Load()     Dim cookie As IFortuneCookie     Dim cookie2 As IFortuneCookie2     Set cookie = New FortuneCookie     Set cookie2 = cookie     cookie2.SetSeed(77) End Sub 

This code creates an instance of a FortuneCookie COM class, calls QueryInterface at least twice—once each for the IFortuneCookie and IFortuneCookie2 interfaces—and automatically handles the reference counting of each interface. Because of the inherent COM run-time support found in Visual Basic and Microsoft Visual J++, developers using those languages are somewhat more able than C++ developers to focus on the problems their software must solve rather than on syntax.

The CComPtr and CComQIPtr Classes

To provide a level of syntactic convenience that more closely approaches the code shown in Listing 5-1, but without the associated overhead expense Visual Basic imposes, ATL provides two smart pointer classes—CComPtr and CComQIPtr—that implement automatic reference counting. (See Chapter 2 for more details about smart pointers.) The term smart pointer—a common but rather controversial notion in the COM community—refers to a lightweight templatized C++ wrapper class that manages the lifetime of an interface pointer. The basic idea behind smart pointers is to encapsulate the calls to AddRef and Release such that the constructor and destructor call them automatically, as shown in this pseudocode:

 template <class P> class CSmartPtr {     P* p; public:     CSmartPtr(P* ptr) { p = ptr; if(p) p->AddRef(); }     ~CSmartPtr() { if(p) p->Release(); } }; 

As you'll see later in this chapter, this technique introduces its own set of complications, but it has two benefits. First, the client code no longer needs to call AddRef and Release when using an instance of the pointer. Second, the client code can represent the interface using a CSmartPtr stack variable instead of a dynamically allocated pointer, which means that Release will automatically be called from within the CSmartPtr destructor when the variable goes out of scope.

The ATL smart pointer classes are much more sophisticated than the implementation shown in the preceding pseudocode example. Using the ATL CComPtr and CComQIPtr classes, the C++ version of the Visual Basic code at the top of the previous page would look like this:

 void OnLoad() {     CComPtr<IFortuneCookie> cookie;     cookie.CoCreateInstance(OLESTR("FortuneLib.Cookie"));     CComQIPtr<IFortuneCookie2> cookie2(cookie);     cookie2->SetSeed(77); } 

Although the C++ version of the OnLoad code is still syntactically more complex than the Visual Basic code it mimics, it can be expressed in fewer lines. Even more important, because cookie and cookie2 are implemented as stack variables rather than pointers, the interfaces they encapsulate are automatically released when OnLoad terminates, whether as a result of normal execution or a thrown exception.

Previous Versions of ATL: CComPtr and CComQIPtr

The CComPtr and CComQIPtr classes were significantly improved from ATL 2.1 to ATL 3.0. As we describe later in this chapter, one of the chief disadvantages of smart pointer encapsulation is that inadvertent calls made directly to the IUnknown::AddRef and IUnknown::Release methods can cause unexpected and disastrous results. To prevent this problem, the ATL 3.0 versions of the CComPtr and CComQIPtr classes include special provisions that prevent users of those classes from calling AddRef and Release using the overloaded indirection operator (->). ATL 3.0 also introduces several new methods to the smart pointer classes, as you'll see in Table 5-1.

Let's take a look at how the ATL smart pointers work. The source for the CComQIPtr class is defined in atlbase.h and is shown in Listing 5-3. The CComPtr and CComQIPtr classes differ only slightly, so the code in Listing 53 essentially represents the functionality of both classes. Because of this similarity, we'll refer to the ATL smart pointer classes as CComPtr unless we're specifically describing the extra methods implemented by CComQIPtr. As shown in boldface in the listing, CComQIPtr has an optional templatized parameter and two methods not found in CComPtr.

NOTE
In day-to-day development, we use CComQIPtr almost exclusively because it contains a superset of the functionality exposed by CComPtr.

Listing 5-3. Source listing for the CComQIPtr class .

 template <class T> class _NoAddRefReleaseOnCComPtr : public T { private:     STDMETHOD_(ULONG, AddRef)()=0;     STDMETHOD_(ULONG, Release)()=0; }; 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() {         IUnknown* pTemp = p;         if(pTemp)         {             p = NULL;             pTemp->Release();         }     }     operator T*() const     {         return p;     }     T& operator*() const     {         ATLASSERT(p!=NULL); return *p;     }     // The assert on operator& usually indicates a bug. If the     //  assertion is really what is needed, however, take the      //  address of the p member explicitly.     T** operator&()     {         ATLASSERT(p==NULL);         return &p;     }     _NoAddRefReleaseOnCComPtr<T>* operator->() const     {         ATLASSERT(p!=NULL);         return (_NoAddRefReleaseOnCComPtr<T>*)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!() const     {         return (p == NULL);     }     bool operator<(T* pT) const     {         return p < pT;     }     bool operator==(T* pT) const     {         return p == pT;     }     // Compare two objects for equivalence.     bool IsEqualObject(IUnknown* pOther)     {         if(p == NULL && pOther == NULL)             return true; // They are both NULL objects.         if(p == NULL || pOther == NULL)             return false; // One object is NULL                           //  but the other isn't.         CComPtr<IUnknown> punk1;         CComPtr<IUnknown> punk2;         p->QueryInterface(IID_IUnknown, (void**)&punk1);         pOther->QueryInterface(IID_IUnknown, (void**)&punk2);         return punk1 == punk2;     }     void Attach(T* p2)     {         if(p)         p->Release();         p = p2;     }     T* Detach()     {         T* pt = p;         p = NULL;         return pt;     }     HRESULT CopyTo(T** ppT)     {         ATLASSERT(ppT != NULL);         if(ppT == NULL)             return E_POINTER;         *ppT = p;         if(p)             p->AddRef();         return S_OK;     }     HRESULT SetSite(IUnknown* punkParent)     {         return AtlSetChildSite(p, punkParent);     }     HRESULT Advise(IUnknown* pUnk, const IID& iid, LPDWORD pdw)     {         return AtlAdvise(p, pUnk, iid, pdw);     }     HRESULT CoCreateInstance(REFCLSID rclsid,         LPUNKNOWN pUnkOuter = NULL, DWORD dwClsContext = CLSCTX_ALL)     {         ATLASSERT(p == NULL);         return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext,             _ _uuidof(T), (void**)&p);     }     HRESULT CoCreateInstance(LPCOLESTR szProgID,         LPUNKNOWN pUnkOuter = NULL, DWORD dwClsContext = CLSCTX_ALL)     {         CLSID clsid;         HRESULT hr = CLSIDFromProgID(szProgID, &clsid);         ATLASSERT(p == NULL);         if(SUCCEEDED(hr))             hr = ::CoCreateInstance(clsid, pUnkOuter, dwClsContext,                 _ _uuidof(T), (void**)&p);         return hr;     }     template <class Q>     HRESULT QueryInterface(Q** pp)     {         ATLASSERT(pp != NULL && *pp == NULL);         return p->QueryInterface(_ _uuidof(Q), (void**)pp);     }     T* p; }; 

The template parameter T—used to specify the type of the interface pointer being encapsulated—is required to allow the CComPtr class to be strongly typed. This strong typing means that rather than using a generic void* type, the class has a member variable of type T* that holds the interface pointer. This technique offers more than just type safety. It allows the wrapper class to act as if it were a pointer to the interface T when dereferenced using the indirection operator (->), like so:

 CComPtr<IBuckDog> smartPtr(pDogPtr); pDogPtr->Bark();    // This calls the IBuckDog::Bark method. smartPtr->Bark();   // So does this! 

In this code snippet, pDogPtr is a pointer to an IBuckDog interface, so using the indirection operator to call its Bark method is obvious and fully expected. What isn't immediately clear to the novice is how calling smartPtr->Bark ends up calling the Bark method in light of the fact that smartPtr doesn't inherit from IBuckDog, nor is it a pointer! The answer to this puzzle is that the CComPtr class overloads the indirection operator, as shown in the following pseudocode:

 T* CComPtr::operator->() const {     return m_ptr;  // Where m_ptr is a member variable of type T* } 

The elegance of C++—combined with the use of templates—allows the CComPtr class to leverage operator overloading to make seamless, type-safe calls to the interface pointer, at the expense of a negligible layer of method invocation.

Using the CComPtr class has several benefits. The CComPtr class allows you to manage the lifetime of an interface pointer as if it were a class pointer by encapsulating the calls to AddRef and Release in its constructors and destructor, respectively. Because smart pointers are typically automatic variables rather than dynamically allocated variables, they are automatically cleaned up when they go out of scope. The use of CComPtr and CComQIPtr simplifies the syntax of the application by reducing the calls to AddRef, Release, and QueryInterface sprinkled throughout the code.

Aside from the benefits mentioned above, the CComPtr class provides helper methods that make it even more convenient to use. Several of these methods, introduced in ATL 3.0, are shown in Table 5-1.

Table 5-1. Frequently Used CComPtr Methods.

CComPtr Method Description
IsEqualObject Compares two interface pointers to see whether they represent the same object. According to the rules of COM, whether two interface pointers represent the same object can be determined only by comparing IUnknown pointers. This function isn't the same as the == operator, which simply performs a mathematical comparison of the value of two pointers.
Attach Attaches a pointer to the class without incrementing its reference count. Presumably, if the pointer is valid, its reference count is already nonzero via a previous call (whether directly or indirectly) to AddRef. If the class already encapsulates a valid pointer, that pointer will be released before being overwritten.
Detach Returns the encapsulated pointer and sets the internal copy to NULL without calling Release.
CoCreateInstance Creates an instance of a coclass specified by its CLSID or its progID, requesting the interface of type T. This method simply calls the global ::CoCreateInstance function, but it does so in a type-safe manner. This method provides default arguments for the outer aggregate and class context.
QueryInterface Allows the client to make QueryInterface calls on the encapsulated pointer in a type-safe manner.

Although there is no inheritance relationship between CComPtr and CComQIPtr, the CComQIPtr class is essentially a superset of the functionality provided by CComPtr. The chief difference between the two classes is that CComQIPtr provides additional constructor and assignment operators that automatically call QueryInterface when passed an interface pointer. As mentioned earlier, these methods are displayed in boldface in Listing 5-3. CComQIPtr allows you to assign one pointer to another—even if the two pointers represent different interfaces—by making a call to QueryInterface under the hood during the assignment.

 CComPtr<IUnknown> ptrUnk; HRESULT hr = ptrUnk.CoCreateInstance(OLESTR("MSDEV.Application")); hr = OleRun(ptrUnk); CComQIPtr<IDispatch> ptrDisp(ptrUnk);    // QI under the hood! CComQIPtr<IApplication> ptrApp; ptrApp = ptrUnk;                         // QI under the hood! if(!application)     // Failed 

In this code, CComQIPtr makes a call to QueryInterface in the dispTest constructor and also in the ptrApp assignment. To allow you to determine whether a smart pointer represents a valid interface pointer, the smart pointer overloads the logical NOT operator, returning true if the pointer is NULL:

bool CComPtr::operator!() const {     return (p == NULL); } 

The Mr. Hyde of Smart Pointers

So far, we've described the advantages of using the CComPtr and CComQIPtr classes. Obviously, they offer a level of convenience and information hiding that makes their use attractive. Smart pointers are not without a dark side, however. They can potentially introduce several problems into your application.

The smart pointer classes provide a CoCreateInstance method that encapsulates a call to the COM API of the same name. This method is helpful in many situations, especially because it has two overloaded implementations: one that takes a progID parameter and another that accepts a CLSID. The ATL smart pointer classes also encapsulate the casting of the interface pointer to a void** (as required by the global ::CoCreateInstance API), thus reducing the errors generally associated with that cast's lack of type safety. The drawback is that the CoCreateInstance method calls ::CoCreateInstance rather than ::CoCreateInstanceEx, so you can't use it when you need to explicitly specify the name and activation security context of the remote machine on which the object will be created. Furthermore, you can't always use the CoCreateInstance method when instantiating Automation servers. Some servers require you to call the ::OleRun API to transition the object into a running state before calling QueryInterface for the desired interface.

NOTE
Microsoft Developer Studio is an example of such an Automation server. Because you must first call QueryInterface for IUnknown, call OleRun, and then call QueryInterface for IApplication, the CComPtr<IApplication>::CoCreateInstance method won't work correctly. Incidentally, the Visual C++ run-time library implements a smart pointer class named _com_ptr that automatically performs these steps in its CreateInstance method.

As mentioned earlier in the chapter, the CComQIPtr class automatically calls QueryInterface on an incoming pointer used during construction or assignment. This feature is convenient, but it hides the return value from the hidden call. Here is the equivalent pseudocode for the assignment operator:

 T* CComQIPtr::operator=(IUnknown* lp) {     IUnknown* pTemp = p;    // p is the member pointer.     p = NULL;     if(lp != NULL)         lp->QueryInterface(riid, (void**)&p);     if(pTemp)         pTemp->Release();     return p; } 

Notice that the HRESULT returned by the call to QueryInterface is completely ignored! For objects created in the same apartment or process as the client, this "cold shoulder" is probably not a big deal because QueryInterface simply returns S_OK upon success or E_NOINTERFACE upon failure. But when the call to QueryInterface crosses apartment or process boundaries, a wide range of HRESULTs might be returned because of the existence of the remote procedure call (RPC) layer. The client code can use the logical NOT operator to test the validity of the pointer, but it has no way of knowing what specifically caused the error.

CComPtr also includes an implicit type cast operator, which generates a fairly common error. The cast allows smart pointers to be assigned to raw pointers. For example, examine the following code:

 void (IBuckDog* pDog) {     CComPtr<IBuckDog> dog;     GetObject(&dog);     *pDog = dog; } 

ATL's implementation allows the last line of the function to compile. However, no AddRef occurs, which is bad.

The last problem that can occur with smart pointers has to do with the use of the CComPtr and CComQIPtr classes being a stylistic preference rather than a necessity. It isn't uncommon to find two developers working on the same project—perhaps even the same routines—who don't both use the smart pointer classes. One developer might prefer using these classes, but the other might be accustomed to calling AddRef and Release. In versions of ATL prior to 3.0, you could directly call the IUnknown::AddRef and IUnknown::Release methods exposed by the encapsulated pointer by using the overloaded indirection operator (->), as shown in this pseudocode:

 void CMyClass::OnLoad() {     CComPtr<ITimeServer> ptrApp;     CoCreateInstance(CLSID_TimeServer, NULL, CLSCTX_ALL,         IID_ITimeServer, (void**) &ptrApp);     ptrApp->Release();    // Oops! This is gonna cause problems! } 

This code leads to disastrous results because the reference count for the interface pointer will have already been manually reduced to 0 before the smart pointer destructor calls Release. Fortunately, the architects of ATL 3.0 found a tricky way to prevent this from happening, so the code shown here will no longer compile. We'll leave it as an exercise for you to figure out how they did it.



Inside Atl
Inside ATL (Programming Languages/C)
ISBN: 1572318589
EAN: 2147483647
Year: 1998
Pages: 127
Authors: Steve Zimmerman, George Shephard, George Shepherd
BUY ON AMAZON

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