Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 19.  Casts


19.8. comstl::interface_cast

Although it's good to keep examples nonproprietary, I think it might be time to get all technology specific. For those few who've not yet heard of COM, I'll give a whistlestop tour of the bits germane to this section. (If you want to get into it in depth there are many good books on the subject [Box1998, Broc1995, Eddo1998]; prepare for a lot of learning![9])

[9] I must confess that I'm very fond of COM. Not DCOM, MTS, OLE, ATL, or __declspec(uuid), but pure boiled-down COM is great, despite its complexity.

COM stands for component object model, and it does exactly what it says on the tin: It's a model for describing and implementing component software, for creating and managing component objects. It is based around the IUnknown interface, which has three methods. AddRef() and Release() are responsible for reference counting. QueryInterface() is the method whereby one can ask a COM object whether it has another nature—the one we're querying for—and request a pointer to that nature. The natures we are talking about are other interfaces derived from IUnknown, identified by unique interface identifiers (IIDs), which are 128-bit numbers.

Listing 19.13.


interface IImpCpp


  : public IUnknown


{


  virtual HRESULT CanSupportOO(BOOL *bOO) = 0;


};


extern const IID IID_IImpCpp;


interface IImpC


  : public IUnknown


{


  virtual HRESULT CanSupportOO(BOOL *bOO) = 0;


};


extern const IID IID_IImpC;



Because a COM object can implement a number of interfaces, and because it is not prescribed that it inherits from any or all of them, it is not appropriate to cast COM interface pointers as we might other inherited types. If we held a pointer to IImpCpp and wished to convert it to IImpC, we cannot cast, since they are not directly related; they only share a parent. (Other important rules within COM prevent the use of dynamic_cast to cross-cast [Stro1997] such a relationship.) In fact you can see from this simple example that if we wished to inherit from both IImpCpp and IImpC, we would have a deal of a time providing differential implementations of their CanSupportOO()methods.[10]

[10] Naturally both methods would return true, as was demonstrated in Chapter 8.

When we want to access another nature of a COM object, therefore, we must ask—query—the current interface for whether it implements the required interface. Hence:

Listing 19.14.


IImpCpp *ic =  . . .;


IImpCpp *icpp;


// Acquire a pointer to the IImpC interface


HRESULT hr = ic->QueryInterface(IID_IImpCpp,


                                reinterpret_cast<void**>(&icpp));


if(SUCCEEDED(hr))


{


  BOOL bOO;


  icpp->CanSupportOO(&bOO);


  icpp->Release(); // Release the interface


}



This is pretty much boilerplate for getting hold of another interface from an existing one. There are two problems associated with this mechanism. First, one must be sure to specify the right IID constant. The second problem is that it is easy to get the cast wrong, usually by forgetting the address of operator, instead passing in reinterpret_cast<void**>(icpp). This only comes out in complete code coverage tests, which are very difficult to achieve in significant projects [Glas2003].

There have been numerous attempts at encapsulating this procedure, some of which involve "smart" pointers but it's not yet been done to (my) satisfaction. Since no one likes a critic who lacks a better alternative I'll put my head on the chopping block and show you my answer: the interface casts.[11]

[11] These form part of COMSTL, an STLSoft subproject devoted to COM, and are included on the CD.

19.8.1 interface_cast_addref

We'll look at one of these in action, and rewrite the example.



IImpC   *ic =  . . .;


IImpCpp *icpp = interface_cast_addref<IImpCpp*>(ic);


if(NULL != icpp)


{


  BOOL bOO;


  icpp->CanSupportOO(&bOO);


  icpp->Release(); // Release the interface


}



Since the interface identifier and the pointer to receive the acquired interface are related to the type of interface required, we deduce both of them from the interface type. We've lost the QueryInterface() call, and its associated gobbledegook, and removed the two common problems. A successful cast results in a non-NULL pointer, which the client code is responsible for releasing when it has finished with it.

This is a nice start, but we still have the remainder of the boilerplate, including the check on conversion success, and the manual release of the interface when we're finished with it.

19.8.2 interface_cast_noaddref

Sometimes all you want to do is make a single method call on an interface, as is the case in our example. In that case, the eight lines of code can be more than a little cumbersome. That's where the second interface cast comes in. Using this cast, we can rewrite it in three lines:



IImpC *ic =  . . .;


BOOL  bOO;


interface_cast_noaddref<IImpCpp*>(ic)->CanSupportOO(&bOO);



As its name suggests, this cast does not effect an AddRef() on the given interface. Actually, it does temporarily increase the object's reference count, via a call to QueryInterface() to acquire the IImpCpp interface, but then it releases that interface again at the end of the statement: thus effecting no net reference count change on the object.

Since this cast performs the interface querying and makes the method call in a single statement, there is no opportunity for error testing in the client code, and so the cast throws an exception—defaulted to bad_interface_cast—if the interface cannot be acquired.

19.8.3 interface_cast_test

As we'll see in Part 7, whether you use exceptions or return values for error handling, you still must take responsibility for failure conditions somewhere along the way. So when using interface_cast_addref we must check for NULL, and when using interface_cast_noaddref, we need to catch bad_interface_cast. Because COM is a binary interface, we cannot throw exceptions out of the link unit (see Chapter 9), so we could be in for a lot of try-catch blocks around the place.

The rules of COM identity [Box1998] require that if an instance has ever returned a given interface in response to a QueryInterface() call, it must always do so throughout its lifetime. Therefore, the third component of this set, interface_cast_test, comes into play. Essentially this is a cast masquerading as a Logical Shim (see section 20.3), which is for use in conditional expressions. It can be used to ensure that a given use of interface_cast_noaddref will not throw an exception, or that an interface_cast_addref will not return NULL. But where it really comes into use is when combined with nontemporary interface_cast_noaddref, as in:



IUnknown  *punk =  . . .;


if(interface_cast_test<IThing*>(punk))


{


  interface_cast_noaddref<IThing*>  thing(punk);


  thing->Method1();


  . . .


  thing->MethodN();


} // dtor of thing will release the reference



Of course, the same could be achieved by creating the nontemporary thing instance, and catching an exception thrown if punk was not convertible to IThing*. But in practice, the creation of COM components is a lightweight exercise, and in some cases in environments where a C/C++ run time library is not available [Rect1999]. In such circumstances, the default exception used by interface_cast_noaddref can be set (via the preprocessor) to one that does not actually throw, allowing the above form to be succinct, safe, and yet lightweight. This may not be "proper" C++ but we're imperfect practitioners, and it's a pragmatic increase in code quality within the limitations of the technological environment.

I'm in the Kernighan and Pike camp [Kern1999] in believing exceptions should be reserved for truly unexpected conditions, so tend to prefer this approach in most circumstances.

19.8.4 Operator Implementations

By now you must be asking how these cast operators work, so let's take a look. All three classes inherit protectedly from the template interface_cast_base, but in slightly different ways. First, interface_cast_noaddref:

Listing 19.15.


template< typename I


        , typename X = throw_bad_interface_cast_exception


        >


class interface_cast_noaddref


  : protected interface_cast_base<I, noaddref_release<I>, X>


{


public:


  typedef interface_cast_base<. . . >   parent_class_type;


  typedef I                             interface_pointer_type;


  typedef . . .                         protected_pointer_type;


public:


  template <typename J>


  explicit interface_cast_noaddref(J &j)


    : parent_class_type(j)


  {}


  explicit interface_cast_noaddref(interface_pointer_type pi)


    : parent_class_type(pi)


  {}


public:


  protected_pointer_type operator -> () const


  {


    return static_cast<. . .>(parent_class_type::get_pointer());


  }


// Not to be implemented


private:


  . . . // Inaccessible copy ctor & copy assignment op


};



The parent class is a specialization of interface_cast_base, based on the noaddref_release functor class and the given exception policy type. This functor is used to ensure that there is no net gain (or loss) to the reference count of the instance being cast, by releasing the interface acquired in the constructor.

The template constructor provides the flexibility to attempt a cast to any COM interface. Since that is an operation with a modest cost, the second constructor, which takes a pointer of the desired type, is implemented to efficiently call AddRef() on the given pointer. In both cases, the constructors defer to their corresponding constructors in the base class.[12]

[12] All of the cast classes provide both constructor versions, rather than just the template one, in order to work correctly with compilers that do not support member template constructors correctly.

The queried interface is accessible only via the operator ->() const method, which helps to ensure the safety of this cast. Since no implicit conversion operator is provided, the compiler will reject code of the following form, which would otherwise represent a dangerous potential dead reference:



IX *px = interface_cast_noaddref<IX>(py); // Compiler error


px->SomeMethod(); // Crash. Can't get here, thankfully!



This is classic hairshirt programming: protecting users of our types from misuse by compilers conspiring with the language to make things too easy. However, it does make the cast difficult to use in a scenario such as the following:



func(IX *px);


IY *py = . . .;


func(interface_cast_noaddref<IX>(p)); // Compile error



The rules of COM say that you don't take ownership of a reference passed to a function (except where that's the explicit semantics of a given function). So, although using interface_cast_addref here would compile, it would leave a dangling reference. interface_cast_noaddref is the one we need.

Of course, where there's a will, there's a way. If you're feeling perverse, you could always write



IStream *pi = interface_cast_noaddref<IStream, . . .>(p).operator ->();


pi->SomeMethod(); // Undefined behavior. May crash!



but then I'd have to call the C++ police, and it would be "No code for you! You come back: one year!" Naturally, because imperfect programming tells us to be respectful of the wishes of (experienced) developers, there is a solution to this. We can use the associated get_ptr() Attribute Shim (we meet the Shims concept in all its glory in the next chapter) as in:



func(get_ptr(interface_cast_noaddref<IX>(p))); // Ok



The implementations of the two other casts are similar to that of interface_cast_noaddref. interface_cast_test uses the inert exception policy type ignore_interface_cast_exception so that failure does not result in an exception being thrown and is, rather, represented by the Boolean implicit conversion operator.[13] interface_cast_test uses noaddref_release to ensure that there is no net reference count change.

[13] This is not the real implementation of the Boolean operator. We see how to do this properly in Chapter 24.

Listing 19.16.


template<typename I>


class interface_cast_test


  : protected interface_cast_base<I, noaddref_release<I>,


                                ignore_interface_cast_exception>


{


  . . .


  operator bool () const


  {


    return NULL != parent_class_type::get_pointer();


  }


  . . .



interface_cast_addref similarly uses the inert exception policy, albeit as a defaulted template policy parameter, but uses addref_release to effect the necessary increase in the reference count. It provides access to the acquired interface via an implicit conversion operator.

Listing 19.17.


template< typename I


        , typename X = ignore_interface_cast_exception


        >


class interface_cast_addref


    : protected interface_cast_base<I, addref_release<I>, X>


{


  . . .


  operator pointer_type () const


  {


    return parent_class_type::get_pointer();


  }


  . . .



19.8.5 Protecting the Reference Count

You've probably noticed that interface_cast_noaddref returned a pointer to the acquired interface in the form of the protected_pointer_type. This is used to prevent client code from making pathological calls to an interface's AddRef() or Release(); since the cast manages the cast interface's lifetime, client code has no business calling these methods. Thus protected_pointer_type is defined in terms of the protect_refcount template,[14] which looks like the following:

[14] Since version 3.0, Microsoft's Active Template Library (ATL) has done the same thing, so it can't be all bad, eh?

Listing 19.18.


template <typename I>


interface protect_refcount


    : public I


{


private:


  STDMETHOD_(ULONG, AddRef)()


  {


    I   *pi = static_cast<I*>(this);


    return pi->AddRef();


  }


  STDMETHOD_(ULONG, Release)()


  {


    I   *pi = static_cast<I*>(this);


    return pi->Release();


  }


};



The two methods are made inaccessible to client code of interface_cast_noaddref, while all the other interface-specific methods remain accessible.[15]

[15] This guarantee is only for compilers that support partial template specialization. But if you regularly compile your source with multiple compilers, as you should, this won't be a problem.



interface_cast_noaddref<IX>(py)->Release(); // Compile error!



19.8.6 interface_cast_base

The three cast classes merely parameterize the base class with appropriate policy classes, and render certain features accessible. All the action happens in interface_cast_base, shown in Listing 19.19.

Listing 19.19.


template< typename I


        , typename R


        , typename X


        >


class interface_cast_base


{


protected:


  typedef I   interface_type;


  typedef R   release_type;


  typedef X   exception_policy_type;


protected:


  template <typename J>


  explicit interface_cast_base(J &j)


    : m_pi(do_cast(j))


  {}


  explicit interface_cast_base(interface_type pi)


    : m_pi(pi)


  {


    addref(m_pi);


  }


  ~interface_cast_base()


  {


    if(NULL != m_pi)


    {


      release_type()(m_pi);


    }


  }


  static interface_type do_cast(LPUNKNOWN punk)


  {


    interface_type  pi;


    if(NULL == punk)


    {


      pi = NULL;


    }


    else


    {


      REFIID  iid = IID_traits<interface_type>().iid();


      HRESULT hr  = punk->QueryInterface(iid,


                             reinterpret_cast<void**>(&pi));


      if(FAILED(hr))


      {


        exception_policy_type()(hr, iid);


        pi = NULL;


      }


    }


    return pi;


  }


  interface_type const  &get_pointer_();


  interface_type        get_pointer_() const;


private:


  interface_type const  m_pi;


private:


  . . . // Inaccessible copy ctor & copy assignment op


};



There are several points to note. First, all methods are protected, so it cannot be (mis)used directly; it can only be used via derived classes. Second, as was discussed earlier, the constructors are defined as a template and nontemplate pair, to support both generality and efficiency. Third, the interface pointer, if non-NULL, is released by the parameterizing release_type functor, thereby effecting the release (or not) as requested by the release_type policy type.

So that leaves the static method do_cast(), which is where all the action is. do_cast() is called from the template constructor in order to try to effect the cast. If the given interface pointer is non-NULL, QueryInterface() is called on it to obtain the required interface. If this succeeds, then the new interface is returned, otherwise the exception_policy_type functor is called. After the exception_policy_type functor is called, the pointer is set to NULL in the cases where the exception_policy_type does nothing.

19.8.7 IID_traits

The only remaining query regarding the implementation is how the interface identifier is determined. Simply, we use the classic technique [Lipp1998] for accessing values from types: traits. The interface identifier traits class, IID_traits, is defined as follows:



template <class I>


struct IID_traits


{


public:


  static REFIID   iid();


};



Hypocrisy alert: on translators that support Microsoft's __uuidof()extension, I go the easy route and define the iid() method (for the general case) as:



template <class I>


inline /* static */ REFIID IID_traits<I>::iid()


{


  return __uuidof(I);


}



For those that do not, no definition is provided for the general case and individual specializations (for both Interface and Interface*) must be defined. A macro—COMSTL_IID_TRAITS_DEFINE()—is provided for this purpose, and all the current standard interfaces are thus specialized in the header comstl_interface_traits_std.h, which is included in these circumstances, in the following manner:



#ifdef __IClassFactory_FWD_DEFINED__


COMSTL_IID_TRAITS_DEFINE(IClassFactory)


#endif  /* __IClassFactory_FWD_DEFINED__ */



Users of the casts whose compilers do not provide __uuidof() must define specializations for their own interfaces in the same way. This is not exactly ideal, but it's not exactly onerous either, and it's pretty foolproof: the lack of a general case prevents any errors getting past the compile phase.

In an idealized sense it's vexing to have to relent from one's principles of eschewing proprietary extensions, but pragmatism wins out over idealism here (and in most other cases, I guess). We should be willing to use such things as long as we are not, and don't become, dependent on them. The goal is to have the highest quality software, not the purest soul!

19.8.8 interface_cast Coda

What a lot of COM! I would have preferred to have a non-technology-oriented example, but it is difficult to synthesize something meaningful to this degree.[16] We've made the querying of interfaces type safe, succinct, and even have policy-based error handling. So what's the catch?

[16] I have to admit that the interface casts hurt my brain while I was creating them!

Well it's not performance: everything is inline, and the code has no difference in performance from the original. There's definitely a significant gain in robustness: we've removed the complexity and buggish tendencies of QueryInterface(). Furthermore, we've reduced the opportunity for reference-count abuse by restricting the availability of the interface (and the AddRef() and Release() methods, as we'll see in a moment) to a functional minimum.

We've made the code clearer by reducing the amount of code and by using cast syntax. A reader of your code can quite clearly see what you're attempting to query for, and also whose responsibility it is to deal with any concomitant reference count increase.

Portability has improved, albeit that it's not perfect. When using custom interfaces for compilers that do not support the __uuidof() extension, one must provide an IID_traits specialization. But the important thing is that, although the use of this extension makes our use of the casts easier, it is not necessary to have the extension, so we still have excellent portability.

It's now more maintainable, because the amount of code has shrunk, and the opportunity to introduce errors has been minimized.

To be honest, the only criticism that the casts get is their names. I recognize that the names are not exactly succinct, but it is very important that people who use the casts, and those who will maintain their code, are presented with as little ambiguity as possible. When we're dealing with reference counting, one too many or too few references can be a significant error. Therefore, I opted for the explicit but ugly naming conventions. I've had other names suggested, but none that preserve the unambiguous semantics of the casts, so it looks like they're staying.


      Previous section   Next section