Introduction
This article discusses the implementation of a smart pointer reference-counting pattern, via a class called Handle
. This implementation substantially improves on the design discussed in my previous article, "Extending the Reference Counting Pattern." When I wrote that article, most of the widely available compilers did not implement C++ templates as well, or as completely, as they do today. Therefore, despite many advantages, the original implementation had two serious limitations promptly pointed out by Scott Meyers in a Letter to the Editor (CUJ, December 1998). It provided a rigid data construction mechanism (worked only with the default constructor) and did not support inheritance-based polymorphism. Considering these limitations, even I became convinced to abandon the idea in favor of a "more conventional" solution, which was more expensive in terms of memory consumption and performance.
Since that time, the widespread availability of powerful new language features, such as member function templates and template constructors, enabled me to revive and improve the original design. This new design overcomes the aforementioned limitations, and represents what I feel is a safe, economical, and flexible smart pointer.
Although there are a great many implementations of the reference-counting pattern (see More Effective C++ and The C++ Programming Language, Third Edition just to name a few), the design discussed in this article offers several advantages, which are perhaps rarely found in one implementation. I list a few of the most important here; you can read about the rest of them in the section of this article entitled "Summary of Advantages and Known Limitations."
-
Handle<Data>
is fully automatic. It createsData
instances for you when and the way you want them, manages the data (using reference counting), and deletes them when you no longer need the data. - It is safe.
Handle<Data>
maintains a strong association with theData
it represents. The existence of aHandle<Data>
instance guarantees the existence and validity ofData
(unlike unassigned instances ofData *
orauto_ptr<Data>
). -
Handle
is template-based rather than inheritance-based, such as the implementations described in Scott Myers' book and Marshall Cline's C++ FAQ. The template-based approach does not impose requirements on theData
class, and thus allows the introduction of reference-counting functionality later in development, or with the use of legacy code. -
Handle
conveniently replaces pointers by closely matching their familiar syntax and behavior, and without the dangers associated with pointers andauto_ptr
.
An Illustration
One of Handle<Data>
's strong points is its convenience. The Handle
wrapper takes complete care of Data
resource management while providing a familiar interface, and without sacrificing flexibility:
class Data { ... // Three ways to create a Data // instance Data(); Data(int, double); Data(const char*); }; // Use Data::Data() Data* p = new Data(); auto_ptr<Data> ap(new Data()); Handle<Data> dh = Handle<Data>::create(); Handle<Data> dh; // Use Data::Data(int, double) Data* p = new Data(1, 2.3); auto_ptr<Data> ap(new Data(1, 2.3)); Handle<Data> dh = Handle<Data>::create(1, 2.3); Handle<Data> dh(1, 2.3); // Use Data::Data(const char*) Handle<Data> dh = Handle<Data>::create("text"); Handle<Data> dh("test");
Unlike auto_ptr
, Handle
creates Data
instances. This approach ensures consistent management of the Data
resource the same Handle
class creates, controls access to, and ultimately deletes Data
. Such encapsulation of functionality eliminates the need for explicit memory management (using new
and delete)
, thus reducing chances for memory mismanagement:
Data* dp; // Unassigned. auto_ptr<Data> ap; // Unassigned. auto_ptr<Data> ap(dp); // Trouble. Handle<Data> dh; // Internally creates a Data instance if the // class has default constructor. Otherwise, // simply does not compile. dp->modify(); // Trouble ap->modify(); // Trouble dh->modify(); // OK
Handle<Data>
instances (and their internal Data
resources) are created using one of the following interfaces:
- a family of template functions
Handle<Data>::create(arguments)
- a family of template constructors
Handle<Data>Handle(arguments)
Both interfaces accept arguments intended for Data
construction and readily transfer them to an appropriate Data
constructor (if such a constructor exists). Therefore, if you want a reference-counted Data
instance to be created with, say, the Data(int, double)
constructor, you simply supply appropriate arguments when a Handle<Data>
instance is created. The arguments you provide are passed to Data(int, double)
. In the transfer from Handle
to Data
, all attributes of the arguments, including const
, are preserved:
class Data { ... // Different constructors for // const and non-const args. Data(const Arg&); Data(Arg&); Data(const Arg*); Data(Arg*); }; Arg arg; const Arg const_arg; // Data::Data(Arg&) called Handle<Data> dh = Handle<Data>::create(arg); Handle<Data> dh(arg); // Data::Data(const Arg&) called Handle<Data> dh = Handle<Data>::create(const_arg); Handle<Data> dh(const_arg); // Data::Data(Arg*) called Handle<Data> dh = Handle<Data>::create(&arg); Handle<Data> dh(&arg); // Data::Data(const Arg*) called Handle<Data> dh = Handle<Data>::create(&const_arg); Handle<Data> dh(&const_arg);
Apart from the decision not to support unassigned-pointer behavior, Handle
has syntax and behavior similar to conventional pointers. Therefore, if you are not sure what to expect from Handle<Data>
, just remember how Data *
pointers would behave in the same situation. The following code shows some of the syntax and behavior similarities using pointers and Handle
s:
// Construction. Data* dp = new Data(...); const Data* cp = new Data(...); Handle<Data> dh = Handle<Data>::create(...); Handle<const Data> ch = Handle<Data>::create(...); // or alternatively Data* dp(new Data(...)); const Data* cp(new Data(...)); Handle<Data> dh(...); Handle<const Data> ch(...); // Non-const-to-const assignments. cp = dp; // OK. ch = dh; // OK. // Const-to-non-const assignments. dp = cp; // Error. Do not compile. dh = ch; // Error. Do not compile. // Inheritance-based polymorphism. // Upcast class Base {...}; class Derived : public Base {...}; class Other {...}; // OK. Base* bp = new Derived(...); // OK. Handle<Base> bh = Handle<Derived>(...); // Error Base* bp2 = new Other(...); // Error Handle<Base> bh2 = Handle<Other>(...); // Downcast Derived* dp = bp; // Error. Handle<Derived> dh = bh; // Error. Derived* dp = dynamic_cast<Derived*>(bp); Handle<Derived> dh = bh.dyn_cast<Derived>(); // Functions accepting Base-based args. void funcA(Base*); void funcB(Handle<Base>); void funcC(const Handle<Base>&); // Passing Handle<Derived> args. funcA(dp); // OK. funcA(dh); // OK. funcB(dh); // OK. funcC(dh); // OK.
Implementation
Listing One shows the main header for the Handle
template. The Handle
class implements a lightweight proxy and manages
data sharing (based on reference counting). The Data
management and reference-counting
infrastructure are encapsulated in a separate Counted
class (line 16).
Listing One: The header file for smart pointers implementation
class HandleBase { proteteced: typedef int Counter; // All Handle::null() instances point at the counter. // It is initialized with the value of 1. Therefore, // it is never 0 and, consequently, there are no // attempts to delete it. static Counter _null; }; template<class Data> class Handle : public HandleBase { #include "counted.h" public: ~Handle() { _counted->dismiss(); } explicit Handle() : _counted(new Counted()) { _counted->use(); } Handle(const Handle<Data>& ref) : _counted(ref._counted) { _counted->use(); } Handle<Data>& operator=(const Handle<Data>& src) { if (this->_counted != src._counted) { _counted->dismiss(); _counted = src._counted; _counted->use(); } return *this; } // Direct access. operator Data& () const { return _counted->operator Data&(); } operator Data* () const { return _counted->operator Data*(); } Data* operator-> () const { return _counted->operator ->(); } #include "create.h" #include "unofficial.h" template<class Other> Handle(const Handle<Other>& ref) : _counted(ref.cast<Data>()._counted) { _counted->use(); } template<class Other> Handle(Handle<Other>& ref) : _counted(ref.cast<Data>()._counted) { _counted->use(); } template<class Other> Handle<Data>& operator=(const Handle<Other>& src) { return operator=(src.cast<Data>()); } // Static cast: // from Handle<Derived> to Handle<Base> // from Handle<Data> to Handle<const Data> // etc. template<class Other> Handle<Other>& cast() { return (_cast_test<Other>(), *(Handle<Other>*) this); } template<class Other> const Handle<Other>& cast() const { return (_cast_test<Other>(), *(Handle<Other>*) this); } // Dynamic downcast: // from Handle<Base> to Handle<Derived> template<class Other> const Handle<Other>& dyn_cast() const { _counted->dyn_cast<Other>(); //test return *(Handle<Other>*) this; } // Special null() instance // to represent unassigned pointer. static Handle<Data> null() { return Handle<Data>((Counted*) &_null); } private: Counted* _counted; Handle(Counted* counted) : _counted(counted) { _counted->use(); } template<class Other> Other* _cast_test() const { return (Data*) 0; } }; #endif
Lines 20-38 show the basic destructor, default constructor, copy-constructor, and assignment operator. Nothing here is new, including the well-established technique for data sharing management (functions use
and dismiss
).
Lines 40-44 show the familiar conversion and access operators. Although I agree that providing an operator Data*
method should not generally be recommended, real life calls for adjustments. On a few occasions I was tempted to take conversion operators out, just to put them back later to interface with third-party or legacy libraries that deal with Data *
pointers directly.
Template versions of a copy constructor and assignment operator (lines 49-68) help Handle
manage polymorphic objects in much the same way as pointers:
class Base {...}; class Derived : public Base {...}; Derived* derived_p; Handle<Derived> derived_h; // Copy-constructors called explicitly Base* base_p(derived_p); Handle<Base> base_h(derived_h); // Copy-constructors called implicitly Base* base_p = derived_p; Handle<Base> base_h = derived_h; // Assignments base_p = derived_p; base_h = derived_h;
These template versions work similarly to their counterparts from the basic set the only difference is that the template versions apply type conversion first. cast<Data>
(lines 75-87) and _cast_test
(lines 117-119) ensure safe static type conversion. _cast_test
simply does not compile if the language does not support the requested type conversion.
The same mechanism provides support for the const
attribute of the Data
type:
Handle<const Data> const_dh; Handle<Data> dh; const_dh = dh;
Since Data
and const Data
are different types, the line const_dh = dh
above causes the template version of the assignment operator to be called (lines 63-68). This assignment operator calls the function Handle<Data>::cast<const Data>
,which in turn calls function Handle<Data>::_cast_test<const Data>
. The compiler is happy with automatic conversion of the Data *
to the const Data*
(line 119) and the assignment goes through. The same mechanism prevents compilation of the statement:
dh = const_dh; // Error.
There are two template copy constructors (lines 49-54 and 56-61). They are very much the same except that the second one (lines 56-61) looks more like a typo to a seasoned C++ programmer it accepts a non-constant reference. The reason for that is not immediately obvious without understanding how Handle
objects are created. Therefore, I'll get back to the unusual copy constructor later.
Two include files (#include
d in lines 46, 47 in Listing
One) define Handle
's interfaces for creation and construction. These
files (Listings Two and Three) have
a lot in common. Both implement the same functionality (they create Handle
s)
and have similar layouts. Both provide template families (create
functions
and Handle
constructors). Each file is divided into separate groups for
different numbers of incoming arguments. Then, to ensure proper handling of
const
argument attribues, every group lists all possible combinations
of the arguments with and without the const
attribute.
Listing Two: Include file that specifies create interface.
// No args: Handle<Data> = Handle<Data>::create(); static Handle<Data> create() { return new Counted(); } // One arg: Handle<Data> dh = Handle<Data>::create(arg1); template<class Arg1> static Handle<Data> create(Arg1& arg1) return new Counted(arg1); } template<class Arg1> static Handle<Data> create(const Arg1& arg1) { return new Counted(arg1); } // Two args: Handle<Data> dh = // Handle<Data>::create(arg1, arg2); #define TEMPLATE template<class Arg1, class Arg2> #define CREATE(Arg1, Arg2) \ static \ Handle<Data> \ create(Arg1& arg1, Arg2& arg2) \ { \ return new Counted(arg1, arg2); \ } TEMPLATE CREATE(const Arg1, const Arg2) TEMPLATE CREATE(const Arg1, Arg2) TEMPLATE CREATE( Arg1, const Arg2) TEMPLATE CREATE( Arg1, Arg2) #undef TEMPLATE #undef CREATE // Three args require 8 functions. #define TEMPLATE template<class Arg1, class Arg2, class Arg3> #define CREATE(Arg1, Arg2, Arg3) \ static \ Handle<Data> \ create(Arg1& arg1, Arg2& arg2, Arg3& arg3) \ { \ return new Counted(arg1, arg2, arg3); \ } TEMPLATE CREATE(const Arg1, const Arg2, const Arg3) TEMPLATE CREATE(const Arg1, const Arg2, Arg3) TEMPLATE CREATE(const Arg1, Arg2, const Arg3) TEMPLATE CREATE(const Arg1, Arg2, Arg3) TEMPLATE CREATE( Arg1, const Arg2, const Arg3) TEMPLATE CREATE( Arg1, const Arg2, Arg3) TEMPLATE CREATE( Arg1, Arg2, const Arg3) TEMPLATE CREATE( Arg1, Arg2, Arg3) #undef TEMPLATE #undef CREATE // Four args require 16 functions. // Five args require 32 functions. // Implement when needed.
Listing Three: Include file that specified constructor-based interface.
// One arg: Handle<Data> dh(arg1); template<class Arg1> explicit Handle(const Arg1& arg1) : _counted(new Counted(arg1)) { _counted->use(); } template<class Arg1> explicit Handle(Arg1& arg1) : _counted(new Counted(arg1)) { _counted->use(); } // Two args: Handle<Data> dh(arg1, arg2); #define TEMPLATE template<class Arg1, class Arg2> #define CONSTRUCTOR(Arg1, Arg2) \ explicit Handle(Arg1& arg1, Arg2& arg2) \ : _counted(new Counted(arg1, arg2)) { _counted->use(); } TEMPLATE CONSTRUCTOR( Arg1, Arg2) TEMPLATE CONSTRUCTOR( Arg1, const Arg2) TEMPLATE CONSTRUCTOR(const Arg1, Arg2) TEMPLATE CONSTRUCTOR(const Arg1, const Arg2) #undef TEMPLATE #undef CONSTRUCTOR // Three args require 8 constructors. #define TEMPLATE template<class Arg1, class Arg2, class Arg3> #define CONSTRUCTOR(Arg1, Arg2, Arg3) \ explicit Handle(Arg1& arg1, Arg2& arg2, Arg3& arg3) \ : _counted(new Counted(arg1, arg2, arg3)) { _counted->use(); } TEMPLATE CONSTRUCTOR(const Arg1, const Arg2, const Arg3) TEMPLATE CONSTRUCTOR(const Arg1, const Arg2, Arg3) TEMPLATE CONSTRUCTOR(const Arg1, Arg2, const Arg3) TEMPLATE CONSTRUCTOR(const Arg1, Arg2, Arg3) TEMPLATE CONSTRUCTOR( Arg1, const Arg2, const Arg3) TEMPLATE CONSTRUCTOR( Arg1, const Arg2, Arg3) TEMPLATE CONSTRUCTOR( Arg1, Arg2, const Arg3) TEMPLATE CONSTRUCTOR( Arg1, Arg2, Arg3) #undef TEMPLATE #undef CONSTRUCTOR // Four args require 16 constructors. // Five args require 32 constructors. // Implement when needed.
Consider the groups dealing with two arguments as an example (lines 28-45 in Listing Two and lines 11-24 in Listing Three). The basic functionality remains the same throughout the files create a new Handle-Counted-Data assembly using provided arguments:
// From create.h (Listing Two) template<class Arg1, class Arg2> static Handle<Data> create(Arg& arg1, Arg2& arg2) { // Implicitly creates a Handle // instance using private // Handle(Counted*) return new Counted(arg1, arg2); } // From unofficial.h (Listing Three) template<class Arg1, class Arg2> explicit Handle(Arg& arg1, Arg2& arg2) : _counted(new Counted(arg1, arg2)) { _counted->use(); }