Client-side ATL Programming
There are basically two sides to ATL—client-side support and object-side support. By far the largest portion of support is on the object side because of all the code that's needed to implement ActiveX controls. However, the client-side support also turns out to be useful and interesting. We'll take a look at the client side of ATL next, with a little detour first to examine C++ templates, which are the cornerstone of ATL.
C++ Templates
The key to understanding ATL is understanding C++ templates. Despite the intimidating template syntax, the concept of templates is fairly straightforward. C++ templates are sometimes called compiler-approved macros, which is an appropriate description. Think about what macros do: When the preprocessor encounters a macro, it looks at the macro and expands it into regular C++ code. But the problem with macros is that they're sometimes error-prone and they're never type-safe. If you use a macro and pass an incorrect parameter, the compiler won't complain but your program might very well crash. Templates, however, are like type-safe macros. When the compiler encounters a template, it will expand the template just as it would a macro. But because templates are type-safe, the compiler will catch any type problems before the user encounters them.
Using templates to reuse code is different from what you're used to with conventional C++ development. Components written using templates reuse code by template substitution rather than by inheriting functionality from base classes. All the boilerplate code from templates is literally pasted into the project.The archetypal example of using a template is a dynamic array. Imagine you need an array for holding integers. Rather than declaring the array with a fixed size, you want the array to grow as necessary. So you develop the array as a C++ class. Then someone you work with gets wind of your new class and says that she needs the exact same functionality. However, she wants to use floating point numbers in the array. Rather than pumping out the exact same code (except for using a different type of data), you can use a C++ template.Here's an example of how you might use templates to solve the problem. The following is a dynamic array implemented as a template:
template <class T> class DynArray {
public:
DynArray();
~DynArray(); // clean up and do memory management
int Add(T Element); // adds an element and does
// memory management
void Remove(int nIndex) // remove element and
// do memory management
T GetAt(nIndex) const;
int GetSize();
private:
T* TArray;
int m_nArraysize;
};
void UseDynArray() {
DynArray<int> intArray;
DynArray<float> floatArray;
intArray.Add(4);
floatArray.Add(5.0);
intArray.Remove(0);
floatArray.Remove(0);
int x = intArray.GetAt(0);
float f = floatArray.GetAt(0);
}
As you can imagine, creating templates is useful for implementing boilerplate COM code, and templates are the mechanism that ATL uses for providing COM support. The previous example is just one of the many uses for templates. Not only are templates useful for applying type information to a certain kind of data structure, but they're also useful for encapsulating algorithms. You'll see how when we take a closer look at ATL.
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 who use C++ classes. We can start by creating the classes CVBDeveloper and CCPPDeveloper:
class CVBDeveloper {
public:
CVBDeveloper() {
}
~CVBDeveloper() {
AfxMessageBox
("I used Visual Basic .NET, 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 VBDeveloper 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 will succeed when in fact you expect it to fail. The client will gleefully proceed, believing all is well. The client will crash, 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. 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, you might like all the developers to do some design work before they begin coding. Consider the earlier Visual Basic developer and C++ developer examples. When the client calls DoTheWork, the developer will get right to coding without proper design, and he'll probably leave the poor clients in a lurch. What you'd like to do is add a generic hook to the developer classes so they make sure the design is done before coding begins.The C++ solution to coping with these problems is the smart pointer.
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 that certain operations are taken care of automatically rather than having mundane, boilerplate-type operations deferred 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. You can 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 Visual Basic, Visual C++, C#, 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. In order to behave like native C++ pointers in other ways, smart pointer classes must implement the other standard operators, such as the dereferencing 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 they actually start to hack. Here's the code that wraps the Visual Basic 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
Although 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 generic operation, hoisting client-side reference count management up into a smart pointer makes sense.Because you're now familiar with COM, 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 chapters, 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 determine 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 referenced. Most COM objects destroy themselves when they're no longer referenced 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're 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 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 pointer. You need to make a copy of the interface pointer, perhaps to pass it as an out parameter. At the native COM level, you must perform several steps. First, you must release the old interface pointer. Then 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 must 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. CComPtr has much of its functionality factored out in a class named CComPtrBase. Let's start by looking at CComPtrBase.
The CComPtrBase Class
CComPtrBase provides a basis for smart pointer classes that use COM-based memory functions. Here's CComPtrBase:
template <class T>
class CComPtrBase
{
protected:
CComPtrBase() throw()
{
p = NULL;
}
CComPtrBase(int nNull) throw()
{
ATLASSERT(nNull == 0);
(void)nNull;
p = NULL;
}
CComPtrBase(T* lp) throw()
{
p = lp;
if (p != NULL)
p->AddRef();
}
public:
typedef T _PtrClass;
~CComPtrBase() throw()
{
if (p)
p->Release();
}
operator T*() const throw()
{
return p;
}
T& operator*() const throw()
{
ATLASSERT(p!=NULL);
return *p;
}
//The assert on operator& usually indicates a bug. If this is really
//what is needed, however, take the address of the p member explicitly.
T** operator&() throw()
{
ATLASSERT(p==NULL);
return &p;
}
_NoAddRefReleaseOnCComPtr<T>* operator->() const throw()
{
ATLASSERT(p!=NULL);
return (_NoAddRefReleaseOnCComPtr<T>*)p;
}
bool operator!() const throw()
{
return (p == NULL);
}
bool operator<(T* pT) const throw()
{
return p < pT;
}
bool operator==(T* pT) const throw()
{
return p == pT;
}
// Release the interface and set to NULL
void Release() throw()
{
T* pTemp = p;
if (pTemp)
{
p = NULL;
pTemp->Release();
}
}
// Compare two objects for equivalence
bool IsEqualObject(IUnknown* pOther) throw()
{
if (p == pOther)
return true;
if (p == NULL || pOther == NULL)
return false; // One is NULL the other is not
CComPtr<IUnknown> punk1;
CComPtr<IUnknown> punk2;
p->QueryInterface(__uuidof(IUnknown), (void**)&punk1);
pOther->QueryInterface(__uuidof(IUnknown), (void**)&punk2);
return punk1 == punk2;
}
// Attach to an existing interface (does not AddRef)
void Attach(T* p2) throw()
{
if (p)
p->Release();
p = p2;
}
// Detach the interface (does not Release)
T* Detach() throw()
{
T* pt = p;
p = NULL;
return pt;
}
HRESULT CopyTo(T** ppT) throw()
{
ATLASSERT(ppT != NULL);
if (ppT == NULL)
return E_POINTER;
*ppT = p;
if (p)
p->AddRef();
return S_OK;
}
HRESULT SetSite(IUnknown* punkParent) throw()
{
return AtlSetChildSite(p, punkParent);
}
HRESULT Advise(IUnknown* pUnk, const IID& iid, LPDWORD pdw) throw()
{
return AtlAdvise(p, pUnk, iid, pdw);
}
HRESULT CoCreateInstance(REFCLSID rclsid,
LPUNKNOWN pUnkOuter = NULL,
DWORD dwClsContext = CLSCTX_ALL) throw()
{
ATLASSERT(p == NULL);
return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext,
__uuidof(T), (void**)&p);
}
HRESULT CoCreateInstance(LPCOLESTR szProgID,
LPUNKNOWN pUnkOuter = NULL,
DWORD dwClsContext = CLSCTX_ALL) throw()
{
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) const throw()
{
ATLASSERT(pp != NULL);
return p->QueryInterface(__uuidof(Q), (void**)pp);
}
T* p;
};
CComPtrBase is a fairly basic smart pointer. Notice the data member p of type T (the type introduced by the template parameter). CComPtrBase's constructor performs an AddRef on the pointer while the destructor releases the pointer—no surprises here. CComPtrBase also has all the necessary operators for wrapping a COM interface. Only the assignment operator deserves special mention. The assignment does a raw pointer reassignment. The assignment operator calls a function named AtlComPtrAssign:
ATLINLINE 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.CComPtrBase's main strength is that it helps you manage the reference count on a pointer to some degree. The next class down the hierarchy is CComPtr—the class you'd use in a real application.
The CComPtr Class
Because CComPtr derives from CComPtrBase, it includes all the interface pointer management functionality of CComPtrBase. CComPtr can help you manage AddRef and Release operations and code layout. 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 coworkers) 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 might not be as elegant a solution as you'd like, however. 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.
The CComQIPtr Class
Here's CComQIPtr's definition:
template <class T, const IID* piid = &__uuidof(T)>
class CComQIPtr : public CComPtr<T>
{
public:
CComQIPtr() throw()
{
}
CComQIPtr(T* lp) throw() :
CComPtr<T>(lp)
{
}
CComQIPtr(const CComQIPtr<T,piid>& lp) throw() :
CComPtr<T>(lp.p)
{
}
CComQIPtr(IUnknown* lp) throw()
{
if (lp != NULL)
lp->QueryInterface(*piid, (void **)&p);
}
T* operator=(T* lp) throw()
{
return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp));
}
T* operator=(const CComQIPtr<T,piid>& lp) throw()
{
return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp.p));
}
T* operator=(IUnknown* lp) throw()
{
return static_cast<T*>(AtlComQIPtrAssign((IUnknown**)&p,
lp, *piid));
}
};
//Specialization to make it work
template<>
class CComQIPtr<IUnknown, &IID_IUnknown> : public CComPtr<IUnknown>
{
public:
CComQIPtr() throw()
{
}
CComQIPtr(IUnknown* lp) throw()
{
//Actually do a QI to get identity
if (lp != NULL)
lp->QueryInterface(__uuidof(IUnknown), (void **)&p);
}
CComQIPtr(const CComQIPtr<IUnknown,&IID_IUnknown>& lp) throw() :
CComPtr<IUnknown>(lp.p)
{
}
IUnknown* operator=(IUnknown* lp) throw()
{
//Actually do a QI to get identity
return AtlComQIPtrAssign((IUnknown**)&p, lp,
__uuidof(IUnknown));
}
IUnknown* operator=(const CComQIPtr<IUnknown,&IID_IUnknown>& lp)
throw()
{
return AtlComPtrAssign((IUnknown**)&p, lp.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 will call 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...
}
CComQIPtr is useful when you want the Java-style or Visual Basic–style type conversions. Notice that the code listed above doesn't require any calls to QueryInterface or Release. Those calls happen automatically.
ATL Smart Pointer Problems
Smart pointers can be convenient in some places (as in the CComPtr example, in which 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 you must be very careful with smart pointers is when you convert 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 that you must 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 it 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 even though smart pointers make some aspects of client-side COM development more convenient, they're not foolproof. You still have to have some knowledge about how smart pointers work if you want to use them safely.