In the latter years of the last millennium there were a number of articles published that explored the design space of “smart pointers”. That was in the days where there were loads of broken implementations available for download from the internet, but working ones – like those in the Loki and Boost libraries – were not to be found.
One of these pointers has come back to haunt me – partly because I wrote it, but also because it meets a set of recurring requirements. I’d not looked at it for years, but a colleague was implementing a “Cheshire Cat” class using Boost’s
shared_ptr
[1] (which is a dubious choice as the copy and assignment semantics are completely wrong for this purpose) so I pointed him to my old article on
grin_ptr
[2]. This article has also been referenced by a couple of recent Overload articles, so I thought it was time to revisit the subject.
The first thing I discovered is that I’ve never submitted the article for publication in Overload (instead a heavily annotated version written with Allan Newton appeared in Overload 38 [3]). The second thing I discovered is that there were a number of improvements I’d like to make to both the code and to the article. What follows is the result.
The “Classical” Cheshire Cat
The “Cheshire Cat” idiom is a great solution to a set of problems that have existed since the pre-history of C++. This idiom first emerged in the late ‘80s in a cross-platform GUI class library called CommonView and was described by John Carolan [4]. It has been reinvented a number of times since and is also known as “Compilation Firewall” and as “Pimpl”. The “Gang of Four” [5] classifies it as a special case of the “Bridge” pattern. These problems addressed by Cheshire Cat stem from three areas:
-
Ensuring that a header file includes nothing that the client code needn’t be aware of. (In the case of CommonView, this includes the native windowing APIs.)
-
Making it possible to distribute updated object code libraries without requiring the client code to be recompiled. If a class definition (or anything else in a header) changes, then any compilation unit that includes it must be recompiled.
-
Protecting intellectual property by concealing implementation techniques used.
Which of these is most important to you will differ according to circumstance, but the “Cheshire Cat” idiom addresses them all.
When Glockenspiel (the suppliers of CommonView) started developing their cross-platform GUI library they were influenced by all three of the above reasons for hiding the implementation. Here is a simplified telephone list class implemented in the style of the CommonView library:
struct phone_list_implementation;
class phone_list
{
public:
phone_list(const char *name);
phone_list(const phone_list& rhs);
phone_list& operator=(const phone_list& rhs);
~phone_list();
const char* list_name();
const char* find_number(const char *person);
void add(const char *name,
const char *number);
private:
phone_list_implementation* grin;
};
Note the need for a copy constructor and assignment operator. These are required because
phone_list
owns the
phone_list_implementation
to which it holds a pointer. (The default behaviour of just copying the pointer value is obviously inappropriate.)
In the implementation file we can imagine that the phone_list member functions are implemented like this:
const char* phonelist::find_number(const char* person)
{
return grin->number(person);
}
The Motivation for grin_ptr
Once you’ve written a few classes using this idiom it will occur to you that you are writing the same functions again and again to manage the implementation. Specifically the copy constructor, assignment operator and destructor of a Cheshire Cat class are generic - they always look the same, they only differ in the types of the interface and implementation classes.
This sounds like a job for a template. A template that looks after an implementation object allocated on the heap, and ensures it is copied or deleted when appropriate. It is tempting to reach for the standard library, but the closest thing we find there is
auto_ptr<>
. Even moving further afield to Boost we don't find a smart pointer with the desired semantics.
Let us suppose for a moment that such a smart pointer exists and is called
arg::grin_ptr<>
. This would allow the
phone_list
class to be rewritten as follows:
class phone_list
{
public:
explicit phone_list(std::string const& name);
std::string name() const;
std::pair<bool, std::string> number(std::string const& person) const;
phone_list& add(std::string const& name, std::string const& number);
private:
class implementation;
arg::grin_ptr<implementation> grin;
};
Note that there is no longer a need to supply the copy constructor, assignment operator, or destructor as we are assuming that the necessary logic is supplied by the
grin_ptr<>
template. (I’ve also taken the opportunity to use a more contemporary style of C++, by nesting implementation, using
const
and
std::string
but the ideas are the same.)
In the implementation file we can imagine that the phone_list member functions are implemented the same way as in the original. For example:
std::pair<bool, std::string>
phonelist::number(std::string const& person) const
{
return grin->number(person);
}
The idea is for
grin_ptr<>
to act “just like” a pointer in all relevant ways while taking the burden of ownership management. Naturally, “all relevant ways” doesn’t include such things as support for pointer arithmetic since this is inappropriate for pointers used in implementing “Cheshire Cat”.
By using a compatible substitute for a “real” pointer we avoid the need to write repetitive boilerplate code. Everything else is the same.
The resulting simplification looks good, but can such a smart pointer be implemented?
Implementing arg::grin_ptr<>
The principle problem to overcome in implementing
grin_ptr<>
is that the compiler generated copy constructor, assignment operator and destructor for
phone_list
will each instantiate the corresponding code for
grin_ptr<>
in a context where
implementation
is an incomplete type.
This means that the
grin_ptr<>
copy constructor cannot simply copy construct an instance of
implementation
. Likewise, the destructor of
grin_ptr<>
can’t be a simple
delete p;
as deleting a pointer to an incomplete type gives undefined behaviour whenever the type in question has a non-trivial destructor. The assignment operator, will either have to deal with both these issues or delegate these problems. Let’s write a test case for these operations:
struct implementation;
int implementation_instances = 0;
int implementation_copies_made = 0;
void test_incomplete_implementation(const arg::grin_ptr<implementation>& const_grin)
{
assert(implementation_instances == 1);
assert(implementation_copies_made == 0);
{
// Check that copy construction works
// creates a new instance
arg::grin_ptr<implementation> nonconst_grin(const_grin);
assert(implementation_instances == 2);
assert(implementation_copies_made == 1);
// Check assignment calls copy constructor
// (and that the discarded implementation
// is deleted)
nonconst_grin = const_grin;
assert(implementation_instances == 2);
assert(implementation_copies_made == 2);
}
// Check destructor cleans up instances
assert(implementation_instances == 1);
}
Note that this test case is unable to initialise
grin_ptr<>
with an instance of
implementation
– this is a direct consequence of
implementation
being an incomplete type.
In implementing
grin_ptr<>
I make use of the fact that the constructor is instantiated in the implementation file for
phone_list
, where the class definition for
implementation
resides. Similarly calls to
implementation
member functions also require a complete type. These are tested in the next part of the test harness:
struct implementation
{
struct const_tag {};
struct nonconst_tag {};
const_tag function() const
{ return const_tag(); }
nonconst_tag function()
{ return nonconst_tag(); }
implementation()
{ ++implementation_instances; }
implementation(const implementation&)
{ ++implementation_instances;
++implementation_copies_made; }
~implementation()
{ --implementation_instances; }
};
void test_implementation()
{
assert(implementation_instances == 0);
assert(implementation_copies_made == 0);
{
arg::grin_ptr<implementation> grin(new implementation);
assert(implementation_instances == 1);
assert(implementation_copies_made == 0);
test_incomplete_implementation(grin);
// Check that copy construction works
// creates a new instance
const arg::grin_ptr<implementation>& const_grin(grin);
assert(implementation_instances == 1);
// if const qualification is wrong then
// these should produce compiler error
implementation::const_tag const_test = const_grin->function();
implementation::nonconst_tag nonconst_test = grin->function();
}
// Check destructor cleans up instance
assert(implementation_instances == 0);
}
If you examine the code for
grin_ptr<>
(shown in listing 1) you’ll see that each instance carries around a pointer to a structure containing function pointers
do_copy
and
do_delete
. This structure is initialised using a trick I first saw used by Richard Hickey [6]: the constructor (which is instantiated with a complete type) instantiates the corresponding
my_delete_ftn
and
my_copy_ftn
template functions and stores the pointers to them. Because these see the full definition of the class used to initialise the pointer they can make use of its copy constructor and destructor (the casts are there to support implementation hierarchies). Using pointers to functions provides a safe method for
grin_ptr<>
to copy and delete the object it owns. The point of passing around function pointers instead of the apparently more natural use of virtual member functions is that everything can be done “by value” and no dynamic allocation is required.
There are few features that can be seen in this implementation that are not immediately related to the foregoing discussion:
-
The dereference and indirection operators are const qualified, which allows the implementation class to overload on constin a natural manner.
-
The single argument (conversion) constructor may be initialised using a class derived from
implementation
– this allows different specialisations ofimplementation
to be selected at runtime 1 . This functionality is illustrated in the test case given in Listing 2. -
There is a two argument constructor that allows the user to provide custom copy and delete functionality.
Conclusion
The class template presented here -
arg::grin_ptr<>
removes some of the repetitive work required when implementing Cheshire Cat classes. In addition it is able to support applications that are considerably more sophisticated (making use of polymorphic implementations and/or overloading on const) than the
phone_list
example considered here.
Alan Griffiths
alan@octopull.demon.co.uk
References
-
“shared_ptr class template”, Greg Colvin et al., http://www.boost.org/libs/smart_ptr/shared_ptr.htm
-
“Ending with the grin”, Alan Griffiths, http://www.octopull.demon.co.uk/arglib/TheGrin.html
-
‘Interpreting “Supporting the “Cheshire Cat” idiom”’, Alan Griffiths & Allan Newton, Overload 38
-
“Constructing bullet-proof classes”, John Carolan, Proceedings: C++ at Work ’89, SIGS
-
Design Patterns, Gamma, Helm, Johnson, Vlissides ISBN 0-20163361-2, Addison-Wesley
-
Callbacks in C++ Using Template Functors, Richard Hickey C ++ Gems ISBN 1 884842 37 2
#ifndef INCLUDED_GRIN_PTR_HPP_ARG_20060308 #define INCLUDED_GRIN_PTR_HPP_ARG_20060308 #include <utility> namespace arg { template<typename Grin> class grin_ptr { public: /// Construct taking ownership of /// pointee. /// CheshireCat must be a complete type, /// Copyable and Destructable. template<typename CheshireCat> explicit grin_ptr(CheshireCat* pointee); /// Copy using copy function grin_ptr(const grin_ptr& rhs); /// Destroy using delete function ~grin_ptr() throw() { copy_delete->do_delete(p); } /// Return contents (const) const Grin* get() const { return p; } /// Return contents (non-const) Grin* get() { return p; } /// Dereference op (const) const Grin* operator->() const { return p; } /// Dereference op (non-const) Grin* operator->() { return p; } /// Dereference op (const) const Grin& operator*() const { return *p; } /// Dereference op (non-const) Grin& operator*() { return *p; } /// Swaps contents with "with" void swap(grin_ptr& with) throw(); /// grin_ptr& operator=(const grin_ptr& rhs); /// Pointers to deep copy and delete /// functions struct copy_delete_ptrs { typedef void (*delete_ftn)(Grin*); typedef Grin* (*copy_ftn)(const Grin*); copy_ftn do_copy; delete_ftn do_delete; }; /// Allow user to specify copy and delete grin_ptr(Grin* pointee, copy_delete_ptrs* cdp) : p(pointee), copy_delete(cdp) {} private: Grin* p; copy_delete_ptrs* copy_delete; template<typename CheshireCat> static void my_delete_ftn(Grin* p); template<typename CheshireCat> static Grin* my_copy_ftn(const Grin* p); }; template<typename Grin> template<typename CheshireCat> inline void grin_ptr<Grin>::my_delete_ftn(Grin* p) { delete static_cast<CheshireCat*>(p); } template<typename Grin> template<typename CheshireCat> inline Grin* grin_ptr<Grin>::my_copy_ftn(const Grin* p) { return new CheshireCat( static_cast<const CheshireCat&>(*p)); } template<typename Grin> template<typename CheshireCat> inline grin_ptr<Grin>::grin_ptr(CheshireCat* pointee) : p(pointee), copy_delete(0) { void(sizeof(CheshireCat)); static copy_delete_ptrs cdp = { &my_copy_ftn<CheshireCat>, &my_delete_ftn<CheshireCat> }; copy_delete = &cdp; } template<typename Grin> inline void grin_ptr<Grin>::swap(grin_ptr& with) throw() { std::swap(p, with.p); std::swap(copy_delete, with.copy_delete); } template<typename Grin> inline grin_ptr<Grin>::grin_ptr(const grin_ptr& rhs) : p(rhs.copy_delete->do_copy(rhs.p)), copy_delete(rhs.copy_delete) { } template<typename Grin> inline grin_ptr<Grin>& grin_ptr<Grin>::operator=(const grin_ptr& rhs) { grin_ptr<Grin>(rhs).swap(*this); return *this; } } namespace std { template<class Grin> inline void swap( ::arg::grin_ptr<Grin>& lhs, ::arg::grin_ptr<Grin>& rhs) throw() { lhs.swap(rhs); } } #endif |
Listing 1 – grin_ptr.hpp |
struct base { protected: base() {} base(const base&) {} }; struct derived1 : base { static int instances; derived1() { ++instances; } derived1(const derived1& src) : base(src) { ++instances; } ~derived1() { --instances; } }; int derived1::instances = 0; struct derived2 : base { static int instances; derived2() { ++instances; } derived2(const derived2& src) : base(src) { ++instances; } ~derived2() { --instances; } }; int derived2::instances = 0; void test_derived() { assert(derived1::instances == 0); assert(derived2::instances == 0); { arg::grin_ptr<base> d1(new derived1); arg::grin_ptr<base> d2(new derived2); assert(derived1::instances == 1); assert(derived2::instances == 1); { // Check that copy works arg::grin_ptr<base> d(d1); assert(derived1::instances == 2); assert(derived2::instances == 1); // Check assignment from d1 d = d1; assert(derived1::instances == 2); assert(derived2::instances == 1); // Check assignment from d2 d = d2; assert(derived1::instances == 1); assert(derived2::instances == 2); } // Check destruction assert(derived1::instances == 1); assert(derived2::instances == 1); } // Check destruction assert(derived1::instances == 0); assert(derived2::instances == 0); } |
Listing 2 – test for polymorphic implementation |
-
This is a difference from the earlier implementation of
grin_ptr
– in that, if the user wanted to derive fromimplementation
, it was necessary for implementation to derive fromarg::cloneable
and to declare the destructor virtual. I feel the current approach is more natural and extensible. ↩