Octopull/C++: Ending with the grin |
---|
Home | C++ articles | Java articles | Agile articles | Bridge lessons |
In C++ "private" is sometimes not private enough. Alan Griffiths examines an old technique and adds some recent ideas.
I started using C++ in the late '80s - these were the early days of MS-Windows on the corporate desktop and MFC had not been offered to the developer community. However, there was a small Irish company Glockenspiel who not only offered an early C++ compiler for DOS/Windows (based on CFront), but also a GUI class library called "CommonView". (This was also available for a number of platforms - including OS/2 and a variety of UNIX variants.)
As well as a being a productive tool for writing portable GUI applications this class library was the first place I encountered the venerable "Cheshire Cat" technique for hiding the implementation of a C++ class. I know John Carollan> was associated with this library, and suspect he first coined the phrase.
In C++ the whole of the class body is seen by client code that #includes the corresponding header file. Even those parts of the class marked "private" are compiled along with the client code. This has three undesirable effects: these parts may require headers to be included that wouldn't otherwise be compiled with the client code, if the private parts (or the additional headers) are changed then the client code must be recompiled, and, in the case of library code, it becomes impossible to substitute a new version of the library without recompiling all of the client code when the implementation but not the interface of a class changes. In a small project these effects are small and are often ignored, but because of the need for additional includes they increase exponentially with the number of files in the system.
There are a number of well known techniques for addressing this
issue but here I am going to concentrate on the "Cheshire Cat"
idiom. (This also known as "pimpl" [sic] and a special case of the
"Bridge" pattern described in "Design Patterns" by Gamma et. al.)
There is a
brief overview of the alternatives on my website. The canonical
implementation of a "Cheshire Cat" class looks like
listing 1. Note that, in addition to any
constructors and methods required for the interface to the class it
is also necessary to implement the copy constructor, assignment
operator and destructor in order to manage the "hidden" body
referred to by the "rep
" pointer.
Listing 1: Example of a "Cheshire Cat" class
// ------------ The header file ------------
/** Telephone list. Example of implementing a minimal telephone
* list using "Cheshire Cat" to hide the implementation.
*/
class phone_list
{
public:
explicit phone_list(std::string name);
phone_list(const phone_list& rhs);
~phone_list();
std::string name() const;
std::pair<bool, std::string> number(std::string person) const;
phone_list& add(std::string name, std::string number);
phone_list& operator=(const phone_list& rhs);
private:
class implementation;
implementation* rep;
};
// ------------ The implementation file ------------
using std::string;
using std::pair;
class phone_list::implementation
{
public:
implementation(std::string initialName) :
list_name(initialName) {}
string name() const { return list_name; }
pair<bool, string> number(string person) const
{
pair<bool, string> rval(false, string());
dictionary::const_iterator i = dict.find(person);
if (i != dict.end())
{
rval = std::make_pair(true, (*i).second);
}
return rval;
}
void add(string name, string number)
{
dict[name] = number;
}
private:
typedef std::map<string, string> dictionary;
string list_name;
dictionary dict;
};
phone_list::phone_list(string name)
: rep(new implementation(name))
{
}
phone_list::phone_list(const phone_list& rhs)
: rep(new implementation(*rhs.rep))
{
}
phone_list& phone_list::operator=(const phone_list& rhs)
{
// Allocate...
implementation* tmp(new implementation(*rhs.rep));
// ...before release...
delete rep;
// ...and update
rep = tmp;
return *this;
}
phone_list::~phone_list()
{
delete rep;
}
string phone_list::name() const
{
return rep->name();
}
pair<bool, string> phone_list::number(string person) const
{
return rep->number(person);
}
phone_list& phone_list::add(string name, string number)
{
rep->add(name, number);
return *this;
}
Considering the phone_list
class as seen by the client
code the
private parts consist of a pointer to an incomplete type: this
requires no additional headers (a naive implementation with
list_name
and dict
as private members
would require <map>).
If a new implementation were provided (e.g. to use a name comparison
method that ignored case and understood "Mc..." ==
"Mac..."), then the header wouldn't need to change. A new version of
the class with methods added to the interface will remain
link-compatible with existing code - even if these methods require
changes to the implementation
. These advantages are
gained
at the cost of writing an extra class containing a few forwarding
functions.
The code in listing 1 isn't quite as I'd have
written it in the '80s - there was no a standard string class (or
pair<>
template) then. But it definitely has
the flavour of the past - each time this idiom is used one needs
to explicitly write out the copy constructor, assignment operator
and destructor in the same way. Only the class names change.
At the beginning of the '90s a tool was introduced into the language to address the sort of generic programming: templates. In the years variable compiler support for templates put off a lot of programmers. However, things have improved a lot since then - largely because the standard library makes heavy use of them.
So what are we looking for? A template that looks after an object allocated on the heap, and ensures it is copied and deleted when appropriate.
As you may have guessed this is coming soon, but before proceeding
to my solution we need a short diversion to discuss the
standard library's auto_ptr<>
template. Although
auto_ptr<>
doesn't
meet our current needs it is instructive to know why
since the reasons are not immediately obvious - a notable expert
(Herb Sutter) stepped into this trap in print recently ("Using
auto_ptr Effectively" C/C++ Users Journal October 1999).
std::auto_ptr<>
By historical accident the standard library provides a single smart
pointer template known as auto_ptr<>
.
auto_ptr<>
has what I will politely describe as
"interesting" copy semantics. Specifically, if one
auto_ptr<>
is assigned (or copy constructed)
from another then they are both changed - the
auto_ptr<>
that originally owned the object
loses ownership and becomes 0. This is a trap for the unwary! There
are situations that call for this behaviour, but on most
occasions that require a smart pointer the copy semantics cause a
problem.
If we tried to replace implementation*
with
auto_ptr<implementation>
in the phone_list
class we'd find that we still need to
write the
copy constructor and assignment operator carefully to avoid an
implementation
being passed from one
phone_list
to
another (with the
consequence that one of the phone_list
s loses its
implementation
).
Worse than this, and the point I believe the Herb Sutter missed: we
would also need to write the destructor - if we don't, the one
the compiler generates for us will not correctly destroy the
implementation
. This is because the client code causes
the
generation of the phone_list
destructor and
consequently
instantiates the auto_ptr<>
destructor without
having seen the
class definition for implementation
. As a result the
destructor of
implementation
is never called. (A good compiler may
give a warning about deleting an incomplete type, but the language
standard requires that the code compiles - although the
results are unlikely to be what the programmer intended.)
OK, I could rewrite the code using auto_ptr<>
but
I'm not going to
since it saves us nothing.
arg::grin_ptr<>
Although the standard library doesn't support out needs it is
possible to write a smart pointer which does. To prove it I've put
one into the "arglib" library on my website. This allows the
phone_list
class to be rewritten as shown in listing
2.
Listing 2: Using arg::grin_ptr<>
class phone_list
{
public:
explicit phone_list(std::string name);
std::string name() const;
std::pair<bool, std::string> number(std::string person) const;
phone_list& add(std::string name, std::string number);
private:
class implementation;
arg::grin_ptr<implementation> rep;
};
Note that there is no longer a need to supply the copy constructor,
assignment operator, or destructor as the necessary logic is
supplied by the grin_ptr<>
template. (I've not
shown the implementation file again since it only differs from listing 1 by the removal of these methods.)
In most cases it doesn't matter, but in addition to the convenience
of not having to rewrite these methods,
grin_ptr<>
is what
Kevlin Henney refers to as a
QUALIFIEDSMARTPOINTER
("Coping with Copying in C++" - Overload 33 ISSN 1354 3172). The
particular convenience this has is that methods on
phone_list
and
implementation
may be overloaded on const
since
dereferencing a const grin_ptr<implementation>
provides a
const implementation
. Naturally it is rare for
const
and non-const
methods to have the
same name, but it does
happen. (For example containers overload begin()
and
end()
on const
so as to provide
const_iterators
when the container
is const
.)
arg::grin_ptr<>
If you have what you are looking for I'll understand if you leave me
now, but if you want to understand how grin_ptr<>
works then there are some interesting issues to be addressed.
Let us start with the last point mentioned in the discussion of
auto_ptr<>
- how to cope with deleting an
incomplete type. The destructor can't be a simple "delete
p;
" because at the point of instantiation the pointer is to
an "incomplete type" and the compiler won't call the destructor.
To avoid this I make use of the fact that the constructor for
grin_ptr<>
is instantiated in the implementation
file, where the class definition for implementation
resides. At this point I force the compiler to generate a deletion
function using a trick I first saw in Richard Hickey's article
"Callbacks in C++ Using Template Functors" (C ++ Gems ISBN 1
884842 37 2): the constructor stores a pointer to this function in
the grin_ptr<>
. This provides a safe method for
the destructor to 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.
A similar function is used for copying the object, so because it
contains two function pointers grin_ptr<>
is a
bit bigger than a raw pointer but this isn't likely to be an issue
in most uses. The complete code for grin_ptr<>
is shown in listing 3.
Listing 3: The code for arg::grin_ptr<>
template<typename p_type>
class grin_ptr
{
// Pointers to utility functions
typedef void (*delete_ftn)(p_type* p);
typedef p_type* (*copy_ftn)(const p_type* p);
public:
explicit grin_ptr(p_type* pointee)
: do_copy(&my_copy_ftn), p(pointee), do_delete(my_delete_ftn) {}
grin_ptr(const grin_ptr& rhs);
~grin_ptr() throw() { do_delete(p); }
const p_type* get() const { return p; }
p_type* get() { return p; }
const p_type* operator->() const { return p; }
p_type* operator->() { return p; }
const p_type& operator*() const { return *p; }
p_type& operator*() { return *p; }
void swap(grin_ptr& with) throw()
{ p_type* pp = p; p = with.p; with.p = pp; }
grin_ptr& operator=(const grin_ptr& rhs);
private:
copy_ftn do_copy;
p_type* p;
delete_ftn do_delete;
static void my_delete_ftn(p_type* p)
{
delete p;
}
static p_type* my_copy_ftn(const p_type* p)
{
return deep_copy(p);
}
};
Copying is in fact a bit more flexible than the current discussion
would indicate, since the copying method has been factored out
into the "arg::deep_copy()
" algorithm but if you do
nothing special then copying will take place using
"new implementation(*p);
" as you probably
expect.
As mentioned in the main text this defaults to copying using
"new implementation(*p);
" but if a
clone()
method (or any other name) needs to be called
this is easy to achieve. The code for deep_copy()
is
shown in listing 4.
Listing 4: The code for
arg::deep_copy()
struct cloneable {};
struct Cloneable {};
template<class p_type>
inline p_type* deep_copy(const p_type* p, const void*)
{
return p ? new p_type(*p) : 0;
}
template<class p_type>
inline p_type* deep_copy(const p_type *p, const cloneable *)
{
return p ? p->clone() : 0;
}
template<class p_type>
inline p_type* deep_copy(const p_type *p, const Cloneable *)
{
return p ? p->makeClone() : 0;
}
template<class p_type>
inline p_type* deep_copy(const p_type* p)
{
return deep_copy(p, p);
}
This means that any class that inherits from
arg::clonable()
will be copied by
"->clone();
", any class
that inherits from arg::Cloneable
will be copied using
"->makeClone()
". (These two versions exist to
support both my currently favoured naming style and that I am
required to use at work.) In addition any class that has a
"deep_copy()
" algorithm defined in a suitable
namespace will be copied using it.
arg::grin_ptr<>
removes some of the repetitive
work from the
development of "Cheshire Cat" classes whilst remaining
flexible enough to support applications that are considerably more
advanced (making use of polymorphic implementations
and/or overloading on const
) than the
phone_list
example considered here.
Copyright 1999 Alan Griffiths