Smart Pointers and Inheritance-Based Type Conversions

Smart pointers are a favorite topic of C++ writers; we follow this tradition. A well-known problem of smart pointers is getting overloaded functions to work properly with inheritance hierarchies. Scott Meyers provides an excellent discussion of this in his More Effective C++, Item 28. Dig up your copy now and read it before continuing. You should also be familiar with the smart reference techniques developed in Implementing Smart References for C++.

The problem

Assume an inheritance hierarchy

    class A            { ... };
    class B : public A { ... };
    class C : public B { ... };
and an independently developed smart pointer class
    template <typename T> class SmartPtr;

It is relatively easy to provide appropriate conversions so that a function expecting a  const SmartPtr<B>&  can be passed a  SmartPtr<C>, while being passed a  SmartPtr<A>  will result in a compile error.

    template <typename T>
    class SmartPtr
    {
    public:
      SmartPtr (T* p = 0) : p_ (p) {}

      template <typename U>
      SmartPtr (const SmartPtr<U>& x)
	: p_ (x.operator->()) {}

      template <typename U>
      SmartPtr& operator= (const SmartPtr<U>& x)
      { p_ = x.operator->(); return *this; }

      T* operator->() const { return  p_; }
      T& operator* () const { return *p_; }

    private:
      T *p_;
    };

We would like to solve the harder problem of avoiding compile errors with function overloading as in this example:

    void f (const SmartPtr<A>&) { ... }
    void f (const SmartPtr<B>&) { ... }
    ...
      f (SmartPtr<C>(new C()));

C++ will not let us assign higher priority to one user-defined conversion than to another, so the call to f is ambiguous. It would not be ambiguous if dumb pointers were used instead.

Scott is uncharacteristically pessimistic. He says,

"Let's stop beating around the bush. What we really want to know is how we can make smart pointer classes behave just like dumb pointers for purposes of inheritance-based type conversions. The answer is simple: we can't. As Daniel Edelson has noted, smart pointers are smart, but they're not pointers."

Edelson's paper is a classic and definitely recommended reading.

A partial solution

Using the techniques developed for smart references, we can come up with a partial solution, one that will allow users to call overloaded functions with the right semantics, at a cost of slightly more effort by the library writers.

The key to the solution is to encode class inheritance information in the smart pointer class hierarchy, as we did with the IsA smart reference classes.

Suppose we had a magic template Base so that

    Base<T>::type
gives us the base class of a given C++ class, or NoBase if no such base class exists.

Then we can sketch a skeleton of a working SmartPtr template implementation as follows:

    template<typename T, typename U=T>
    class SmartPtr : public SmartPtr<T, typename Base<U>::type>
    { ... };

    template <typename T>
    class SmartPtr<T,T> : public SmartPtr<T, typename Base<T>::type>
    { ... private: T* p; };

    template <typename T> class SmartPtr<T,NoBase> {};

When this is instantiated with our A,B,C hierarchy, a smart pointer hierarchy is generated that looks like this:

    class SmartPtr<A,A> : public SmartPtr<A,NoBase> { ... };
    class SmartPtr<B,B> : public SmartPtr<B,A>      { ... };
    class SmartPtr<B,A> : public SmartPtr<B,NoBase> { ... };
    class SmartPtr<C,C> : public SmartPtr<C,B>      { ... };
    class SmartPtr<C,B> : public SmartPtr<C,A>      { ... };
    class SmartPtr<C,A> : public SmartPtr<C,NoBase> { ... };

Then our overloaded function f can be written like this:

    template <class T> inline void f (const SmartPtr<T,A>& x) { ... }
    template <class T> inline void f (const SmartPtr<T,B>& x) { ... }

This added capability comes at no cost in complexity to user code that doesn't use it. Users can simply ignore SmartPtr's defaulted second template parameter and continue to work as before.

But how is the magic class Base implemented? Alas, there seem to be limits to the static introspection capabilities of C++ templates. A template can discover whether one class derives from another given class, but cannot generate such a class as a typedef. So the inheritance information will have to be provided explicitly:

    template <> struct Base<B> { typedef A type; };
    template <> struct Base<C> { typedef B type; };
These "inheritance declarations" can be provided in a modular fashion, externally to the definition of SmartPtr or the A,B,C class hierarchy. Failure to provide the declarations is not disastrous — only the function overloading feature is lost.

The careful reader will have noticed that we have ignored multiple inheritance. We believe this can be handled, but requires significantly more template machinery than we want to develop here.

A working minimal SmartPtr implementation incorporating these ideas looks like this:

    class NoBase {};

    template <typename T> struct Base { typedef NoBase type; };

    template<typename T, typename U=T>
    class SmartPtr : public SmartPtr<T, typename Base<U>::type>
    {
    private:
      template <typename, typename> friend class SmartPtr;
      SmartPtr () {}
      const SmartPtr<T>& self () const
      { return static_cast<const SmartPtr<T>&>(*this); }
    public:
      T* operator->() const { return self().operator->(); }
      T& operator* () const { return self().operator* (); }
    };

    template <typename T>
    class SmartPtr<T,T> : public SmartPtr<T, typename Base<T>::type>
    {
    public:
      SmartPtr (T* p = 0) : p_ (p) {}

      template <typename U, typename V>
      SmartPtr (const SmartPtr<U,V>& x)
	: p_ (x.operator->()) {}

      template <typename U, typename V>
      SmartPtr& operator= (const SmartPtr<U,V>& x)
      { p_ = x.operator->(); return *this; }

      T* operator->() const { return  p_; }
      T& operator* () const { return *p_; }

    private:
      T *p_;
    };

    template <typename T> class SmartPtr<T,NoBase> {};

Do we actually use these techniques? Well... We try to practice pointer-less programming and so we avoid using smart pointers. But we like smart references and use related techniques with those.

A sample program is available here.


Back to Martin's home page
Last modified: Thu Jan 16 19:06:24 PST 2003
Copyright © 2003 Martin Buchholz