Calling COM Objects with Smart Interface Pointers

Dale Rogerson
Microsoft Developer Network Technology Group

October 6, 1995

Click to open or copy the files for the SICLIENT sample application for this technical article.

Abstract

This article examines using C++ smart interface pointers to access Component Object Model (COM) interfaces. The SIClient sample application is associated with this article.

Introduction

A long time ago, in the dark, dark past, Nigel Thompson wrote a series of technotes on OLE programming called "OLE for Idiots" (MSDN Library, Technical Articles). His shouts of anguish at forgetting to correctly AddRef or Release an interface could be heard up and down the hallway. I thought there had to be some way to use smart pointers in C++ to automatically Release the Component Object Model (COM) components, making the use of COM components much easier. However, I started working on the then-unreleased Microsoft® Foundation Class Library (MFC), where reference counting wasn't a concern because it was hidden inside the MFC classes.

After writing the article "MFC/COM Objects 8: Revisiting Multiple Inheritance Without MFC," I decided to look again at using a smart pointer class to simplify using COM interfaces. The results of this investigation didn't live to up my expectations, and I doubt I will use smart interface pointers in my own code. However, your COM project might be different from mine, and my reasons for deciding not to use a COM component might not affect you.

In this article, I will discuss the following topics:

In the source code in this article, I use the prefix pI to refer to a pointer to an interface, such as:

IPersist* pIPersist ;

I use the prefix SI to refer to a smart interface pointer:

CSmartInterface<IPersist> SIPersist ;

Reasons for Creating a Smart Interface Pointer Class

The main reason I wanted a smart interface pointer class was to automatically AddRef and Release the interface pointer.

When using COM interface pointers, you have to follow several rules. First, never call the delete operator on an interface pointer; call Release instead. The following code is incorrect:

IDraw* pIDraw ;
CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;
pIDraw->Draw(0,0,100,100) ;
delete pIDraw ; // Don't delete an interface pointer.

The following code is correct:

IDraw* pIDraw ;
CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;
pIDraw->Draw(0,0,100,100) ;
pIDraw->Release() ;

C++ programmers normally delete an object pointer. For this reason, it is easy for C++ programmers to forget and call delete instead of Release on an interface pointer. It is also a hard error for C++ programmers to find, because deleting pointers is so natural.

The second rule is to call AddRef when creating a new pointer. The following code is incorrect:

IDraw* pIDraw ;
CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;
IDraw* pIDraw2 = pIDraw ;
pIDraw->Draw() ;
pIDraw->Release() ; 
pIDraw2->TurnLeft(5) ;

The following is the correct version:

IDraw* pIDraw ;
CoCreateInstance(...,IID,_IDraw, (void**)&pIDraw) ;
IDraw* pIDraw2 = pIDraw ;

pIDraw2->AddRef() ;

pIDraw->Draw() ;
pIDraw->Release() ; 
pIDraw2->TurnLeft(5) ;
pIDraw->Release() ;

The above example is exceedingly trivial; however, in complicated code, this error can be difficult to track down.

These are not the only reasons for using a smart interface pointer class. Don Box covers some other reasons in his column in C++ Report (see the bibliography at the end of this article).

How to Build a Smart Interface Pointer Class

A smart interface pointer class starts life the same way as a smart pointer class: by implementing the operator–> for a class. This process is also referred to as delegation. By overriding operator->, we can make a class that simulates a pointer call. For example:

void Draw()
{
    CSmartInterface<IDraw> SIDraw;
    CoCreateInstance(...,IID,_IDraw, (void**)&SIDraw) ;
    SIDraw->Draw() ;
}

There are several things to notice in the above code. First, a template class is used to implement the smart interface pointer. This makes the smart pointer interface class type-safe. Second, operator& has been overloaded, as we will soon see, to return the address of the pointer contained in CSmartInterface. Third, even though SIDraw isn't a pointer, we use operator-> to call the members in the IDraw interface. Fourth, we don't call Release because CSmartInterface is created on the stack and the destructor will automatically call Release.

Below are the significant parts of the CSmartInterface header. All of the member functions and operators are defined inline later in the header file.

template <class I>
class CSmartInterface
{
public:
   // Construction
   CSmartInterface(I* pI = NULL) ;

   // Copy Constructor
   CSmartInterface(const CSmartInterface<I>& rSI) ;

   // Destruction
   ~CSmartInterface() ;

   // Assignment from I*
   CSmartInterface<I>& operator=(I* pI) ;

   //
   // Operators
   //

   // Conversion
   operator I*() ;

   // Deref
   I* operator->() ;

   // Address of
   I** operator&() ;

   // Equality
   BOOL operator==(I* pI) const;

   // Inequality
   BOOL operator!=(I* pI) const;

   // Negation
   BOOL operator!() const ;

  protected:
   I* m_pI ;
};

CSmartInterface contains a pointer to an interface. It is defined as a template function in order to be type-safe. The meat of CSmartInterface is overriding operator–>:

template <class I> inline 
I* CSmartInterface<I>::operator->()
{
    ASSERT(m_pI!=NULL) ;
    return m_pI ;
}

Therefore, SIDraw–>Draw() from the previous examples results in a call to SIDraw.m_pI->Draw(). SIDraw delegates the Draw call to the interface pointed to by m_pI. The power of this approach is that the CSmartInterface<IDraw> class does not need to be changed every time a new function has been added to the IDraw interface. However, as we will see, CSmartInterface<IDraw> can't stop individual calls to the IDraw interface.

To make CSmartInterface a believable simulation of a native C++ pointer, more operators need to be defined than just operator–>. In fact, the most difficult thing about making a smart pointer class is ensuring that all of the operators used on a pointer are defined and make sense. For example, when I converted the following code from

if (pISimple == NULL) ... ;

to

if (SISimple == NULL) ... ;

I had yet to define operator== for my smart pointer class. The resulting code compiles without error; however, it contains a bug because it compares NULL with SISimple and not with SISimple.m_pI as I had intended. After I defined operator==, this bug went away. The index to your favorite C++ programming book should list all the operators you need to define, thus serving as a convenient checklist. For security, I define private versions of the operators I don't think I need—that way I'll get an error message if I try to use them. An article like Robert Mashlan's "Checked Pointers for C++" in the C/C++ Users Journal can really help with understanding smart pointers (see the bibliography at the end of this article).

Overriding most of the needed operators is straightforward. The most interesting is operator=.

template <class I> inline 
CSmartInterface<I>& CSmartInterface<I>::operator=(I* pI)
{
   if (m_pI != pI) //OPTIMIZE: Same pointer AddRef/Release not needed.
   {
      if (m_pI != NULL)
         m_pI->Release() ; //Smart Pointers don't use smart pointers :-)
      
      m_pI = pI ;

      if (m_pI != NULL)
         m_pI->AddRef() ;
   }      
   return *this ;
}

operator= will automatically AddRef and Release the interfaces for you. If CSmartInterface already points to an interface, it will release it and AddRef the new interface. This definition of  operator= allows the following operations:

void DrawThemAll()
{
   CSmartInterface<IDraw> SIDraw ;
   for (int i = 0 ; i < MAX ; i++ )
   {
      SIDraw = pIDraw[i] ;
      SIDraw->Forward(x) ;
   }
}

The above code depends on CSmartInterface's destructor to release the pointer, which it does:

template <class I> inline 
CSmartInterface<I>::~CSmartInterface()
{
   if (m_pI != NULL)
   {
      m_pI->Release();
   }
}

We can take more advantage of the destructor with a proper constructor:

template <class I> inline
CSmartInterface<I>::CSmartInterface(I* pI /*=NULL*/)
   : m_pI(pI)
{
   if (m_pI != NULL) 
   {
      // AddRef if we are copying an existing interface pointer.
      m_pI->AddRef() ;
   }
}

Now our example can change to:

void DrawThemAll()
{
   for (int i = 0 ; i < MAX ; i++ )
   {
      CSmartInterface<IDraw> SIDraw(pIDraw[i]) ;
      SIDraw->Forward(x) ;
   }
}

The above code runs through a list of IDraw interface pointers, AddRefing them, using them, and releasing them.

There is a lot more that you can do with this. Don Box took the idea further in his column: He defined his equivalent of CSmartInterface to take the interface's ID (IID) in addition to its type. He then defined a constructor that would call QueryInterface if the assignment was from a different interface:

CSmartInterface(IUnknown* pI)
   : m_pI(NULL)
{
   if (pI != NULL)
      pI->QueryInterface(*piid, (void**)&m_pI) ;
}

The above constructor allows us to change our example to:

void DrawThemAll()
{
   for (int i = 0 ; i < MAX ; i++ )
   {
      CSmartInterface<IDraw, &IID_Draw> SIDraw(pIUnknown[i]) ;
      SIDraw->Forward(x) ;
   }
}

This example starts to show the power of the technique. The above code is similar to:

void DrawThemAll()
{
   IDraw* pIDraw ;
   for (int i = 0 ; i < MAX ; i++ )
   {
      pIUnknown[i]->QueryInterface(IID_Draw, (void**)&pIDraw) ;
      pIDraw->Forward(x) ;
      pIDraw->Release() ;
   }
}

operator= can be extended in the same manner, so that assignments such as

SIDraw = pIUnknown ;

would call QueryInterface. I'm not a big fan of hidden implementation behind innocent-looking operators; however, I have to say that overloading operator= in this manner is very persuasive. Visual Basic® version 4.0 calls QueryInterface when assigning one COM object to another.

Using the Smart Interface Pointer Class

There are two main rules for using CSmartInterface. First, don't call Release on a CSmartInterface object:

void Draw()
{
    CSmartInterface<IDraw> SIDraw;
    CoCreateInstance(...,IID,_IDraw, (void**)&SIDraw) ;
    SIDraw->Draw() ;
    SIDraw->Release() ; // Will compile, but is a bug.
}

When SIDraw–>Release is called, SIDraw–>m_pI is released. SIDraw–>m_pI is released once again when its destructor is called, if the interface hasn't already been released. This problem is not that hard to debug. If you have reference counting problems, you can just search for all occurrences of Release. Another approach is to use

#define Release BOGUS_DO_NOT_CALL_RELEASE!

so that using Release generates an error. Of course, if your application has other functions with the word "Release" in them (like many of the Win32® application programming interfaces [APIs]), this method will not work.

The second rule is to avoid using CSmartInterface objects as pointers. It gets very confusing if you do:

CSmartInterface<ISimple>* pSISimple 
      = new CSmartInterface<ISimple>(m_pISimple) ;
(*pSISimple)->Inc() ;
delete pSISimple ;

Using a typedef will clean this up a little bit:

typedef CSmartInterface<ISimple> CSmartISimple ;
CSmartISimple* pSISimple = new CSmartISimple(m_pISimple) ;
(*pSISimple)->Inc() ;
delete pSISimple ;

However, this doesn't change the fact that (*pSISimple)–>Inc() ; actually isn't very (simple, that is!).

If we look at why we want to have a pointer to an interface, we might find a way around the problem. We would like to release the interface at some arbitrary point in our program—not when the CSmartInterface object goes out of scope. The question then becomes, How do we release the interface contained in CSmartInterface? The answer is pretty simple:

SISimple = NULL ;

Although this is very pretty C code, it isn't at all obvious what is happening here. Both

pISimple->Release() ;

and

delete pSISimple ;

are much more obvious ways to indicate that the object is going away.

Reasons Why I Don't Like the Smart Interface Pointer Class

There are several reasons why I don't plan on using the smart interface pointer class. The gist of all of these reasons is that CSmartInterface doesn't feel like C++. It is really strange to use operator-> on an object and not a pointer to an object.

A related reason is that using a pointer to a CSmartInterface is not straightforward—in fact, it can be very confusing. Most of my COM components use containers of cached interface pointers, and the lifetime of the interface is seldom limited to the scope of a function. I call QueryInterface for an interface, store it in the container, use it, and finally release it—all of this from different places in my code. With this type of structure, I need to be able to allocate and deallocate the smart interface on the stack, which, as I demonstrated in the previous section, can be very confusing.

A C++ programmer is likely to delete an interface pointer. A COM programmer is likely to Release a CSmartInterface object. There isn't any convenient way to prevent this. So, our solution has replaced one problem with an equivalent, similar problem. Of course, the set of C++ programmers is larger than the set of COM programmers, at least for now.

I have decided to use interface wrappers instead of smart interface pointers. Interface wrappers are described in my article "Calling COM Objects with Interface Wrappers."

Conclusion

Smart interface pointers are a powerful technique that can make working with COM objects easier and more bug-free. However, I find smart interface pointers to be very strange creatures. They aren't exactly pointers and they aren't exactly objects. They also don't fit my application structure as well as I had expected. However, I strongly suggest that you try using smart interface pointers in your application and see how they work for you. They might be just the ticket you need to get your application out the door more quickly and with fewer bugs.

Bibliography

Box, Don. "Component Craft: Interface pointers considered harmful." C++ Report (Sept. 95): p. 46.

Coplien, James O. Advanced C++ Programming Styles and Idioms. Reading, MA: Addison-Wesley, 1991. (Look under "Delegation" in the index.)

Mashlan, Robert. "Checked Pointers for C++." C/C++ Users Journal, (Oct. 95, Vol. 14, No. 10): p. 37.

Stroustrup, Bjarne. The C++ Programming Language. 2d ed. Reading, MA: Addison-Wesley, 1991. Section 7.9, "Dereferencing," p. 244.

Stroustrup, Bjarne. The Design and Evolution of C++. Reading, MA: Addison-Wesley, 1994. Section 11.5.1, "Smart Pointers," p. 241.

Tip   Search for "Smart Pointers" in the Visual C++ version 4.0 Books Online documentation. Query results should include "Programming Techniques: Templates: When should you use Templates: Smart Pointers."