Safe and Economical Reference-Counting in C++

Smart pointers keep getting smarter.


June 01, 2000
URL:http://www.drdobbs.com/cpp/safe-and-economical-reference-counting-i/184401243

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

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:

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 Handles:

// 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 (#included 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 Handles) 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();
}

The functionality is replicated for all possible const and non-const combinations of the two arguments (lines 39-42 in Listing Two and lines 18-21 in Listing Three). These sets of functions help to deliver arguments to an appropriate Data constructor without losing const attributes. For example,

class Data
{ ...
   // Subtly different constructors.
   Data(const Arg1&, const Arg2&);
   Data(      Arg1&, const Arg2&);
};

Arg1             arg;
const Arg2 const_arg;

Handle<Data> dh(arg, const_arg);

The object dh is created with Handle(Arg1&, const Arg2&) (line 19 in Listing Three), which invokes and transfers the arguments to Counted(Arg1&, const Arg2&) (line 57 in Listing Four), which creates an internal Data instance with the Data constructor that best matches provided argument types (line 53 Listing Four). In the example above the constructor will be Data(Arg1&, const Arg&).

Lisiting Four: The Counted class implementation.

 class Counted
 {
    public:
 
   ~Counted() {}
    Counted() : _num_references(0), _instance() {}
 
    void dismiss () { if (!--_num_references) delete this; }
    void     use () { ++_num_references; }
 
    operator   Data& () { return  _instance; }
    operator   Data* () { return &_instance; }
    Data* operator-> () { return &_instance; }
 
    template<class Derived>
    void dyn_cast() const
    {
       dynamic_cast<Derived&>(_instance);
    }
 
    template<class Derived>
    void dyn_cast()
    {
       dynamic_cast<Derived&>(_instance);
    }
 
    private:
 
    typedef unsigned int uint;
 
    mutable uint _num_references; // Reference counter.
    Data               _instance;
 
    Counted(const Counted&); // Not implemented 
 
    public: // New stuff.
 
    // One argument.
 
    template<class Arg1>
    Counted(const Arg1& arg1) 
    : _num_references(0), _instance(arg1) {}
 
    template<class Arg1>
    Counted(Arg1& arg1) 
    : _num_references(0), _instance(arg1) {}
 
    // Two args.
 
 #define TEMPLATE template<class Arg1, class Arg2>
 #define CONSTRUCTOR(Arg1, Arg2)     \
    Counted(Arg1& arg1, Arg2& arg2)  \
    : _num_references(0), _instance(arg1, arg2) {}
 
 TEMPLATE CONSTRUCTOR(const Arg1, const Arg2)
 TEMPLATE CONSTRUCTOR(const Arg1,       Arg2)
 TEMPLATE CONSTRUCTOR(      Arg1, const Arg2)
 TEMPLATE CONSTRUCTOR(      Arg1,       Arg2)
 
 #undef TEMPLATE
 #undef CONSTRUCTOR
 
    // Three args require  8 constructors.
    // Four  args require 16 constructors.
    // Five  args require 32 constructors.
    // Implement when needed.

 };

The simplified versions of the include files (Listings Two and Three) handle up to three arguments. Although the files are likely to grow (according to the maximum number of incoming arguments you need to support), they have a very regular and easy-to-follow structure. Add support for more arguments when you need it.

It is the very general nature of Handle template constructors (Listing Three) that makes it possible to use the following syntax:

// Creates internal Data instance
// using Data::Data(int)
Handle<Data> dh(1);

// Creates internal Data instance
// using Data::Data(int, double)
Handle<Data> dh(1, 2.3);

Unfortunately, that friendly Data management syntax (it specifies how internal Data are created) overlaps with the syntax reserved for Handle copy-constructors. For example:

class Data
{
   ...
   Data(Handle<Other>);
};

// Create a new Handle-Other pair (a new
// Other instance and the first Handle
// pointing to it).
Handle<Other> oh(args);

// Create a new Handle-Data pair (a new
// Data instance and the first Handle
// pointing to it) using
// Data(Handle<Other>) constructor.
Handle<Data> dh = 
   Handle<Data>::create(oh);

// The following four lines do not 
// create new Handle-Data pairs (as the
// previous lines do) but rather create
// additional handles that point to the
// same data of the Other type as oh
// points to.

// Uses "unusual" template 
// copy-constructor.
Handle<Data> dh1(oh);   // 1.
// Uses basic copy-constructor.
Handle<Data> dh2(dh1);  // 2.
// Uses "unusual" template 
// copy-constructor.
Handle<Data> dh3 = oh;  // 3.
// Uses basic copy-constructor.
Handle<Data> dh4 = dh1; // 4.

Handle copy constructors are partial specializations of

template<class Arg1> 
Handle(const Arg1&);
template<class Arg1> Handle(Arg1&);

declared in unofficial.h (lines 3-9 in Listing Three). Therefore, the dh1-dh4 handles shown above are merely copies of oh. They are created using Handle copy constructors and point to the data initially created together with oh. What's more, if Other is not derived from Data, the lines will even fail to compile.

For this reason I introduced the Handle::create(...) functions — to provide a consistent interface for construction. It is the only interface that is able to create a new Handle-Data pair with a Data(Handle<...>) constructor.

That unfortunate inconsistency (and the only one, to my knowledge) "dethroned" the friendly Handle constructor-based interface and made it "unofficial." Nevertheless, I do prefer and use that syntax for its brevity and expressiveness. (Just keep in mind the special case.)

A Not-So-Conventional Pointer

Despite being similar to conventional pointers, Handle<Data> has a far stronger association with Data it represents. For the sake of performance and safe Data resource management, Handle<Data> is solely responsible for the complete life cycle of an associated Data instance — creation, access, and deletion. Therefore, a Handle<Data> instance guarantees the existence and validity of Data:

Data*          dp; // Unassigned, unusable.
auto_ptr<Data> ap; // Unassigned, unusable.
Handle<Data>   dh; // Creates a Data instance if the
                   // class has default constructor.
                   // Else, simply does not compile.
dp->modify(); // Trouble
ap->modify(); // Trouble
dh->modify(); // OK

The strong bond between Data and Handle<Data> supports the notion that objects should be declared when they are needed and, therefore, initialized. It differs from unassigned C-style declarations. This difference must be remembered when making the transition from raw pointers and auto_ptrs to Handles.

Data*          dp1, dp2, dp3;
auto_ptr<Data> ap1, ap2, ap3;
Handle<Data>   dh1, dh2, dh3;

Although the three lines look quite similar, the third one is far from being a mere replacement for the first two. This line actually creates three Handle<Data> instances together with Data instances. Thus, it is a proper replacement for:

Data* dp1 = new Data();
Data* dp2 = new Data();
Data* dp3 = new Data();

I understand that under rare circumstances the initialize-when-declared rule is difficult and/or inefficient to enforce. If that is the case, the function Handle::null (lines 103-108 in Listing One) comes to the rescue:

// Create an empty Handle instance
// No Data are associated with the 
// handle
Handle<Data> h = Handle<Data>::null();

h->access_data(); // Trouble.
...
if (something)
   h = Handle<Data>::create(arg1);
else
   h = Handle<Data>::create(arg2, arg3);

h->access_data(); // OK.

Handle::null returns a special Handle instance — an analog of null pointer — that is not associated with any data. The instance is potentially dangerous, as attempts to access non-existing data are obviously not valid. Therefore, the construction of the instance is explicit and highly visible. So, if you use Handle::null instances and face a mysterious memory corruption problem, start looking for Handle::null calls and then make sure that the corresponding handles are used properly.

Performance

Additional functionality carries additional performance overhead. The cost of passing a Handle<Data> to a function is roughly twice as much as simply passing a Data * pointer. In other words, it takes twice as long to call an empty func(Handle<Data>) as to call an empty func(Data*). Most often the overhead is negligible comparing to the application's overall operational costs. For example, when sorting of an array of ten integer elements, the function qsort is roughly 100 times as expensive as the overhead required to pass in a pointer to the array.

Also, it is often possible to pass Handle by reference. A func(const Handle<Data>&) call does not activate the reference-counting mechanism and does not incur any additional overhead. However, Handle was not designed to be passed by reference as a general technique; you have to understand the implications of doing so.

To Be Developed

For various reasons, I left out some functionality that I would still like to mention briefly.

Environment

The code for this article was developed and tested on SPARC Solaris 2.6 using gcc-2.95.2. I wouldn't be surprised if older compilers failed to support some of the newer features of the C++ Standard used by this implementation.

Summary of Advantages and Known Limitations

The Handle<Data> implementation here has several advantages over more traditional implementations. The following list is a recap of the advantages mentioned at the beginning of this article:

In addition, Handle has some advantages not yet mentioned:

Following are a couple of limitations of the current design:

However, from my experience, multiple inheritance is an exceptionally rare beast. For my applications, the benefits of Handle generally outweigh this deficiency. However, if your situation is different, you must consider other alternatives.

Such a system will fail to handle destruction properly. In this case, there will be a memory leak when the handles go out of scope, because neither will be able to destroy its owned instance of A.

Additional References

Marshall Cline. C++ FAQ (part 5 of 9). http://www.faqs.org/faqs/C++-faq/part5 and http://www.cerfnet.com/~mpcline/c++-faq-lite/freestore-mgmt.html#[16.22].

Paul T. Miller. "Reference-Counted Smart Pointers and Inheritance," C++ Report, October 1999

Greg Colvin. Specifications for auto_ptr and counted_ptr. comp.std.c++, posted on 25 May 1995.


Vladimir Batov is a senior software engineer currently working for Raytheon Systems Company (Marlborough, MA). During his 16-year career, he has participated in various software development projects including a full-scale nuclear power station simulator, Air Traffic Control systems, and high-availability communication, monitoring, and financial systems in Unix using C/C++. Batov has written several other articles on C++ programming for C/C++ Users Journal. He can be reached at [email protected].

June 2000/Safe and Economical Reference-Counting in C++/Listing 2

Listing 2: Include file that specifies create interface

 1 // No args: Handle<Data> = Handle<Data>::create();
 2 
 3 static
 4 Handle<Data>
 5 create()
 6 {
 7    return new Counted();
 8 }
 9 
10 // One arg: Handle<Data> dh = Handle<Data>::create(arg1);
11 
12 template<class Arg1>
13 static
14 Handle<Data>
15 create(Arg1& arg1)
16 {
17    return new Counted(arg1);
18 }
19 
20 template<class Arg1>
21 static
22 Handle<Data>
23 create(const Arg1& arg1)
24 {
25    return new Counted(arg1);
26 }
27 
28 // Two args: Handle<Data> dh = 
   // Handle<Data>::create(arg1, arg2);
29 
30 #define TEMPLATE template<class Arg1, class Arg2>
31 #define CREATE(Arg1, Arg2)             \
32    static                              \
33    Handle<Data>                        \
34    create(Arg1& arg1, Arg2& arg2)      \
35    {                                   \
36       return new Counted(arg1, arg2);  \
37    }
38 
39 TEMPLATE CREATE(const Arg1, const Arg2)
40 TEMPLATE CREATE(const Arg1,       Arg2)
41 TEMPLATE CREATE(      Arg1, const Arg2)
42 TEMPLATE CREATE(      Arg1,       Arg2)
43 
44 #undef TEMPLATE
45 #undef CREATE
46 
47 // Three args require 8 functions.
48 
49 #define TEMPLATE template<class Arg1, class Arg2, class Arg3>
50 #define CREATE(Arg1, Arg2, Arg3)              \
51    static                                     \
52    Handle<Data>                               \
53    create(Arg1& arg1, Arg2& arg2, Arg3& arg3) \
54    {                                          \
55       return new Counted(arg1, arg2, arg3);   \
56    }
57 
58 TEMPLATE CREATE(const Arg1, const Arg2, const Arg3)
59 TEMPLATE CREATE(const Arg1, const Arg2,       Arg3)
60 TEMPLATE CREATE(const Arg1,       Arg2, const Arg3)
61 TEMPLATE CREATE(const Arg1,       Arg2,       Arg3)
62 TEMPLATE CREATE(      Arg1, const Arg2, const Arg3)
63 TEMPLATE CREATE(      Arg1, const Arg2,       Arg3)
64 TEMPLATE CREATE(      Arg1,       Arg2, const Arg3)
65 TEMPLATE CREATE(      Arg1,       Arg2,       Arg3)
66 
67 #undef TEMPLATE
68 #undef CREATE
69 
70 // Four  args require 16 functions.
71 // Five  args require 32 functions.
72 // Implement when needed.

June 2000/Safe and Economical Reference-Counting in C++/Listing 3

Listing 3: Include file that specified constructor-based interface

 1 // One arg: Handle<Data> dh(arg1);
 2 
 3 template<class Arg1>
 4 explicit Handle(const Arg1& arg1)
 5 : _counted(new Counted(arg1)) { _counted->use(); }
 6 
 7 template<class Arg1>
 8 explicit Handle(Arg1& arg1)
 9 : _counted(new Counted(arg1)) { _counted->use(); }
10 
11 // Two args: Handle<Data> dh(arg1, arg2);
12 
13 #define TEMPLATE template<class Arg1, class Arg2>
14 #define CONSTRUCTOR(Arg1, Arg2) \
15    explicit Handle(Arg1& arg1, Arg2& arg2) \
16    : _counted(new Counted(arg1, arg2)) { _counted->use(); }
17 
18 TEMPLATE CONSTRUCTOR(      Arg1,       Arg2)
19 TEMPLATE CONSTRUCTOR(      Arg1, const Arg2)
20 TEMPLATE CONSTRUCTOR(const Arg1,       Arg2)
21 TEMPLATE CONSTRUCTOR(const Arg1, const Arg2)
22 
23 #undef TEMPLATE
24 #undef CONSTRUCTOR
25 
26 // Three args require  8 constructors.
27 
28 #define TEMPLATE template<class Arg1, class Arg2, class Arg3>
29 #define CONSTRUCTOR(Arg1, Arg2, Arg3) \
30    explicit Handle(Arg1& arg1, Arg2& arg2, Arg3& arg3) \
31    : _counted(new Counted(arg1, arg2, arg3)) 
        { _counted->use(); }
32 
33 TEMPLATE CONSTRUCTOR(const Arg1, const Arg2, const Arg3)
34 TEMPLATE CONSTRUCTOR(const Arg1, const Arg2,       Arg3)
35 TEMPLATE CONSTRUCTOR(const Arg1,       Arg2, const Arg3)
36 TEMPLATE CONSTRUCTOR(const Arg1,       Arg2,       Arg3)
37 TEMPLATE CONSTRUCTOR(      Arg1, const Arg2, const Arg3)
38 TEMPLATE CONSTRUCTOR(      Arg1, const Arg2,       Arg3)
39 TEMPLATE CONSTRUCTOR(      Arg1,       Arg2, const Arg3)
40 TEMPLATE CONSTRUCTOR(      Arg1,       Arg2,       Arg3)
41 
42 #undef TEMPLATE
43 #undef CONSTRUCTOR
44 
45 // Four  args require 16 constructors.
46 // Five  args require 32 constructors.
47 // Implement when needed.

June 2000/Safe and Economical Reference-Counting in C++/Listing 4

Listing 4: The Counted class implementation

 1 class Counted
 2 {
 3    public:
 4 
 5   ~Counted() {}
 6    Counted() : _num_references(0), _instance() {}
 7 
 8    void dismiss () { if (!--_num_references) delete this; }
 9    void     use () { ++_num_references; }
10 
11    operator   Data& () { return  _instance; }
12    operator   Data* () { return &_instance; }
13    Data* operator-> () { return &_instance; }
14 
15    template<class Derived>
16    void dyn_cast() const
17    {
18       dynamic_cast<Derived&>(_instance);
19    }
20 
21    template<class Derived>
22    void dyn_cast()
23    {
24       dynamic_cast<Derived&>(_instance);
25    }
26 
27    private:
28 
29    typedef unsigned int uint;
30 
31    mutable uint _num_references; // Reference counter.
32    Data               _instance;
33 
34    Counted(const Counted&); // Not implemented 
35 
36    public: // New stuff.
37 
38    // One argument.
39 
40    template<class Arg1>
41    Counted(const Arg1& arg1) 
42    : _num_references(0), _instance(arg1) {}
43 
44    template<class Arg1>
45    Counted(Arg1& arg1) 
46    : _num_references(0), _instance(arg1) {}
47 
48    // Two args.
49 
50 #define TEMPLATE template<class Arg1, class Arg2>
51 #define CONSTRUCTOR(Arg1, Arg2)     \
52    Counted(Arg1& arg1, Arg2& arg2)  \
53    : _num_references(0), _instance(arg1, arg2) {}
54 
55 TEMPLATE CONSTRUCTOR(const Arg1, const Arg2)
56 TEMPLATE CONSTRUCTOR(const Arg1,       Arg2)
57 TEMPLATE CONSTRUCTOR(      Arg1, const Arg2)
58 TEMPLATE CONSTRUCTOR(      Arg1,       Arg2)
59 
60 #undef TEMPLATE
61 #undef CONSTRUCTOR
62 
63    // Three args require  8 constructors.
64    // Four  args require 16 constructors.
65    // Five  args require 32 constructors.
66    // Implement when needed.

67 };

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.