Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 26.  What's Your Address?


26.2. What Actions Are Carried Out during Conversion?

Since it's a function like any other, the operator &() overload can do things other than simply return a converted value. This has serious consequences.

Imperfection: Overloading operator &() breaks encapsulation.


That's a bold statement. Let me illustrate why it is so.

As I've mentioned already, ATL has a large number of wrapper classes that overload operator &(). Unfortunately, there are different semantics to their implementations. The types shown in Table 26.1 all have an assertion in the operator method to ensure that the current value is NULL.

Don't worry about the specifics of the types TYPEATTR, VARDESC, and FUNCDESC—they're POD open type structures (see section 4.4) used for manipulating COM meta data. The important thing to note is that they have allocated resources associated with them, but they do not provide value semantics, which means that they must be managed carefully in order to prevent resource leaks or use of dangling pointers.

The operator is overloaded in the wrapper classes to allow these types to be used with COM API functions that manipulate the underlying types, and to be thus initialized. Of course, it's not an initialization as we RAII-phile C++ types know and love it, but it is initialization, because the assertion means that any subsequent attempt to repeat the process will result in an error, in debug mode at least. I'll leave it up to you to decide whether that, in and of itself, is a good way to design wrapper classes, but you can see that you are required to look inside the library to see what is going on. After all, it's using an overloaded operator, not calling a function named get_one_time_content_pointer().[1]

[1] Of course, in an ideal world one would only have to read the documentation to understand, and memorably absorb, the fine nuances of the libraries such as those mentioned in this section. However, this is anything but the case. Documentation is at least one conceptual step away from the code-face, out of date the moment it's written, and difficult to write (either by the author of the code, who knows too much, or by another party, who knows too little). In reality, the code is often the documentation [Glas2003].

The widely used CComBSTR class, which wraps the COM BSTR type, also overloads operator &() to return BSTR*, but it does not have an assertion. By contra-implication, we assume that this means that it's okay to take the address of a CComBSTR multiple times, and, since the operator is non-const, that we can make multiple modifying manipulations to the encapsulated BSTR without ill-effect. Alas, this is not the case. CComBStr can be made to leak memory with ease:



void SetBSTR(char const *str, BSTR *pbstr);





CComBSTR        bstr;





SetBSTR("Doctor", &bstr);   // All ok so far


SetBSTR("Proctor", &bstr);  // "Doctor" is now lost forever!



Table 26.1.

Wrapper Classes

Operator &() return type

CComTypeAttr

TYPEATTR**

CComVarDesc

VARDESC**

CComFuncDesc

FUNCDESC**

CComPtr / CComQIPtr

T**

CHeapPtr

T**


We can surmise that the reason CComBSTR does not assert is that it proved too inconvenient. For example, it is not uncommon to see in COM an API function or interface method that will take an array of BSTR. Putting aside the issue of passing arrays of derived types (see sections 14.5; 33.4), we might wish to use our CComBSTR when we're only passing one string.

An alternative strategy is to release the encapsulated resource within the operator &() method. This is the approach of another popular Microsoft COM wrapper class, the Visual C++ _com_ptr_t template. The downside of this approach is that the wrapper is subject to premature release on those occasions when you need to pass a pointer to the encapsulated resource to a function that will merely be using it, rather than destroying it or removing it from your wrapper. You may think that you can solve this by declaring const and non-const overloads of operator &(), as in Listing 26.2.

Listing 26.2.


template <typename T>


class X


{


  . . .


  T const *operator &() const


  {


    return &m_t;


  }


  T *operator &()


  {


    Release(m_t);


    m_t = T();


    return &m_t;


  }



Unfortunately, this won't help, because the compiler selects the overload appropriate to the const-ness of the instance on which it's to be called, rather than on the use one might be making of the returned value. Even if you pass the address of a non-const X<T> instance to a function that takes T const *, the non-const overload will be called.

To me, all this stuff is so overwhelmingly nasty that I stopped using any such classes a long time ago. Now I like to use explicitly named methods and/or shims to save me from all the uncertainty. For example, I use the sublimely named[2] BStr class to wrap BSTR. It provides the DestructiveAddress() and NonDestructiveAddress() methods, which, though profoundly ugly, don't leave anyone guessing as to what's going on.

[2] This was a definite case of not thinking before coding. The names BSTR and BStr are far too alike, and have caused me not a little bother.


      Previous section   Next section