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++.

Assume an inheritance hierarchy

and an independently developed smart pointer classclassA { ... };classB :publicA { ... };classC :publicB { ... };

template<typename T>classSmartPtr;

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>classSmartPtr {public: SmartPtr (T* p = 0) : p_ (p) {}template<typename U> SmartPtr (constSmartPtr<U>& x) : p_ (x.operator->()) {}template<typename U> SmartPtr&operator= (constSmartPtr<U>& x) { p_ = x.operator->();return*this; } T*operator->()const{returnp_; } 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:

voidf (constSmartPtr<A>&) { ... }voidf (constSmartPtr<B>&) { ... } ... f (SmartPtr<C>(newC()));

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*.

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>::typegives us the base class of a given C++ class, or

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

template<typename T, typename U=T>classSmartPtr :publicSmartPtr<T, typename Base<U>::type> { ... };template<typename T>classSmartPtr<T,T> :publicSmartPtr<T, typename Base<T>::type> { ...private: T* p; };template<typename T>classSmartPtr<T,NoBase> {};

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

classSmartPtr<A,A> :publicSmartPtr<A,NoBase> { ... };classSmartPtr<B,B> :publicSmartPtr<B,A> { ... };classSmartPtr<B,A> :publicSmartPtr<B,NoBase> { ... };classSmartPtr<C,C> :publicSmartPtr<C,B> { ... };classSmartPtr<C,B> :publicSmartPtr<C,A> { ... };classSmartPtr<C,A> :publicSmartPtr<C,NoBase> { ... };

Then our overloaded function `f` can be written like this:

template<classT>inlinevoidf (constSmartPtr<T,A>& x) { ... }template<classT>inlinevoidf (constSmartPtr<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:

These "inheritance declarations" can be provided in a modular fashion, externally to the definition oftemplate<>structBase<B> {typedefA type; };template<>structBase<C> {typedefB type; };

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:

classNoBase {};template<typename T>structBase {typedefNoBase type; };template<typename T, typename U=T>classSmartPtr :publicSmartPtr<T, typename Base<U>::type> {private:template<typename, typename>friendclassSmartPtr; SmartPtr () {}constSmartPtr<T>& self ()const{returnstatic_cast<constSmartPtr<T>&>(*this); }public: T*operator->()const{returnself().operator->(); } T&operator* ()const{returnself().operator* (); } };template<typename T>classSmartPtr<T,T> :publicSmartPtr<T, typename Base<T>::type> {public: SmartPtr (T* p = 0) : p_ (p) {}template<typename U, typename V> SmartPtr (constSmartPtr<U,V>& x) : p_ (x.operator->()) {}template<typename U, typename V> SmartPtr&operator= (constSmartPtr<U,V>& x) { p_ = x.operator->();return*this; } T*operator->()const{returnp_; } T&operator* ()const{return*p_; }private: T *p_; };template<typename T>classSmartPtr<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