Click here to Skip to main content
15,878,970 members
Articles / Programming Languages / C++

A Sensible Smart Pointer Wrap for Most of Your Code

Rate me:
Please Sign up or sign in to vote.
4.68/5 (10 votes)
30 Apr 2014CPOL16 min read 42K   369   40   19
Introducing smart observers of single owners and the concept of Public and Private scope visibility

Introduction

This is a simple smart pointer library that offers the pointer safety that many seek in using std::shared_ptr/weak_ptr or garbage collection but instead is modeled on single ownership and preserves all the virtues of single ownership. It is firmly based on two paradigms:

  1. Smart observers of single owners (introduced in two previous articles XONOR pointers: eXclusive Ownership & Non Owning Reference pointers and Smart pointers for single owners and their aliases)
  2. Public and Private scope visibility (introduced for the first time here)

It is entirely embodied by two smart pointers, each with Public and Private variants:

  1. owner_ptr<T> the exclusive owner
  2. ref_ptr<T> an intelligent observer of a single owner

I believe it is a practical solution with a wide field of application and a low cost of adoption.

Smart Observers of Single Owners

We have smart owners such as auto_ptr<T> and unique_ptr<T> in the standard library but we do not have smart observers of them. We have to make do with raw pointers for any other reference to the object and these can be left dangling. The task of ensuring that everything that was ever informed about the object ... is also informed when the object is deleted, requires unreasonable diligence and is very error prone. Furthermore, not getting this right is a frequent cause of intractable problems as a C++ project develops complexity and fear of this can severely limit design.

The concept of smart observers already exists for shared ownership with std::shared_ptr<T> the shared owner and std::weak_ptr<T> the observer. The owner_ptr<T> and ref_ptr<T> introduced in previous articles are the single ownership equivalent of this and operate through mutual access to autonomous reference counters. However, unlike the std::weak_ptr<T>, ref_ptr<T> is a first class smart pointer that supports direct dereference ->. making it very convenient to use.

Image 1

These smart pointers have been successful in that in my work they have prevented large complex projects being stalled by mysterious invalid pointer bugs at the program infrastructure level and this is without any compromise of single ownership, deterministic destruction or RAIID.

As I worked on this, I discovered a secondary but very important benefit. Once you have provided the owner (in this case owner_ptr) and the observer (in this case ref_ptr) with all the necessary conversions so that they can work together and prohibit operations between them that make no sense, you are left with a new type sensitivity based on the distinction between owner and observer which the compiler can check for correct usage. This keeps your code coherent and clear.

Public and Private Scope Visibility

I am writing this third article because I had been evangelising comprehensive use of these smart pointers but in practice was finding many situations where there is clearly no need for the sophistication of reference counting and to use it would be a pointless overhead. So I started looking for clarity on what specific condition requires the use of reference counting. It is quite simple:

  • If an owner is to be visible outside of the scope in which it is declared, then it must be reference counted along with any observers of it. I define this as Public scope visibility. An autonomous reference counter is required because not only the object but also the owner smart pointer may disappear while observers of them still exist.
  1. N.B. It should be noted that each element of a dynamic collection lives in its own scope so any reference to it is Public to its scope and must be reference counted if its safety is to be guaranteed at all times (you can, of course, just use a raw pointer if you are going to do something quick with it that doesn't disturb the collection). Image 2
  • If an owner is only visible within the scope in which it is declared, then there is no reason for it to be reference counted and observers can safely take the form of a const reference to the owner smart pointer. I define this as Private scope visibility. As it is only visible within its own scope, it will always exist as long as anything that references it. The object may disappear but the smart pointer is always there saying so. Qualifying the reference as a const makes it a proper observer, unable to carry out the role of owner.

Image 3

The Smart Pointers owner_ptr<T> and ref_ptr<T>

Here, I take the owner_ptr (single owner) and ref_ptr (observer) of previous articles, strip them of some obsessive safety features that hinder casual adoption and present them in a more lightweight form as the Public variant:

owner_ptr<T, Public> and ref_ptr<T, Public>

and also provide a zero overhead Private variant

owner_ptr<T, Private> and ref_ptr<T, Private>

  1. also expressed as the default: owner_ptr<T> and ref_ptr<T>

The Public (reference counted) variant will compile in all contexts but the Private (zero overhead) variant will not compile if it is put to Public use. It is logical therefore to make them Private by default and only make them Public where circumstances require it (notifiable by compiler errors). This sensibility of the Private variants to scope visibility is provided by the design of ref_ptr<T, Private> which is a wrapper for const owner_ptr<T, Private>&, a C++ reference that must be initialised on declaration. As it is const by nature, it can never point at anything that doesn't already exist and as owner_ptr<T, Private> can only be declared by value (one of its properties), it will always exist for as long as the ref_ptr<T, Private>.

Both Public and Private variants follow the same essential grammar of interaction, illustrated here with the Private variant:

C++
owner_ptr<T> apT(new T);         // apT owns the object and guarantees to delete                 // it.
if(apT)                                 //is valid or tests as NULL
        apT->DoSomething();             //behaves just like a pointer
apT=NULL;                               //deletes the object
                                //deletes the object when it goes out of scope

ref_ptr<T> rT( apT);             //rT observes the same object while it lives
if(rT)                                  //is valid or tests as NULL – knows when object has been deleted
        rT->DoSomething();              //behaves just like a pointer
rT=NULL;                                //forgets about the object
                                //does not delete the object when it goes out of                 //scope 

The enforcement of correct usage is underpinned by disallowing assignment between owner_ptr<T>s and the treacherous implicit transfer of ownership often associated with it:

C++
owner_ptr<T> apT(new T);
owner_ptr<T> apT2( apT); //Compiler error two owners can never own the same object

and any attempt to point a ref_prt<T> at something not yet owned by an owner_ptr<T>

C++
ref_ptr<T> rT(new T); //Compiler error you cannot observe an object that has no owner

is also disallowed.

Transfer of Ownership

Transfer of ownership can be carried out explicitly.

The yield() dot method will release ownership so it can be consumed by another owner.

C++
owner_ptr<T> apT(new T);
owner_ptr<T> apT2((apT.yield()); //Zeroes existing observing ref_ptr s
                    //They loose contact on transfer
                        //of ownership 

The swap(...) dot method will swap two owners of identical types.

C++
owner_ptr<T> apT(new T);
owner_ptr<T> apT2.swap(apT); //Zeroes existing observing ref_ptr s
                    //They loose contact on transfer
                        //of ownership 

The Public Variants

The Public variants are needed wherever a ref_ptr has a wider scope than the owner_ptr it references. Most trivially:

C++
ref_ptr<T, Public> rT;
{
    owner_ptr<T, Public> apT(newT);
    rT=apT; //this would not compile if either pointer were declared with
        //Private scope visibility
    rT->DoSomething();
} 
if(rT) //Tests as NULL, apT has gone out of scope and object has been deleted
    rT->DoSomething(); //not called 

Real life examples happen when one functional module needs to hold a direct reference to something that was created dynamically in another module and may not always exist. Very typically, these are elements of dynamic arrays.

The Public variant has a couple of extra features:

  1. Ownership of an object can be transferred without zeroing all observing references to it:

    owner_ptr<T> apT(new T);
    owner_ptr<T> apT2( apT.yield_with_refs()); //Conserves existing observing ref_ptr s
                      //They continue to reference the object until 
                        //the new owner destroys it. 
    
    apT2.swap_with_refs( apT); //Conserves existing observing ref_ptr s
                      //They continue to reference the object until 
                        //the new owner destroys it.  
  2. Safe access to the 'this' pointer is provided by the add-on class:

    gives_ref_ptr<T>

    with the public method:

    ref_ptr<T> ref_ptr_to_this()

    In contrast to the shared_ptr/weak_ptr equivalent, ref_ptr_to_this() is a public method and is indifferent to how the object is owned. This means that when a class definition has gives_ref_ptr<T> in its inheritance list then you can declare class objects by value and still be able to call ref_ptr_to_this() on them to get a safe ref_ptr<T, Public>.

    class CClass : public gives_ref_ptr< CClass >
    {
    
    };
    
    CClass object;
    ref_ptr< CClass >rObj= object. ref_ptr_to_this(); 

    You should think about why you should want to do this because most of the time it would make more sense to use a C++ reference:

    CClass object; 
    CClass&  Obj= object;

    However to initialize a reference to a value member of anything dynamically created is unsafe – this is usually indicated by a pointer dereference, a return value or the [] operator in the initialization.

    CParent 
    {
        CClass object;
    };
    vector< CParent > v;
    v.set_size(12);
    CClass&  Obj=v[8].object; //unsafe

    And in these cases taking a ref_ptr<T, Public> is a good solution

    ref_ptr< CClass, Public >rObj= v[8].object. ref_ptr_to_this();

    if v[8] is removed or moved then rObj will test as zero.

    It is not always possible or desirable to alter a class definition to inherit from gives_ref_ptr<T>.

    An alternative is to use the super class template super_gives_ref_ptr<T>in the class object declaration.

    super_gives_ref_ptr<CClass> object;
    ref_ptr< CClass >rObj= object. ref_ptr_to_this();  

Collections of owner_ptr that Make Temporary Copies

There is a third variant of owner_ptr designed specifically so that it can be stored in collections, such as those of STL that may force creation of temporary copies of their elements. You will find that neither the Public nor Private variants of owner_ptr will compile under these circumstances because their prohibition of assignment will not allow those temporary copies to be made. Whenever this occurs, owner_ptr<T, ElementType> can be used as the array element type instead and it will compile and perform correctly. It is reference counted with Public scope visibility and can be referenced by a ref_ptr<T, Public> in exactly the same way as an owner_ptr<T, Public>.

C++
std:vector<owner_ptr<T, ElementType> > v;
v.resize(5);
v[0]=new T;
ref_ptr<T, Public> rA=v[0]; 

It is a hybrid smart pointer, a variant of owner_ptr<T, Public>, that permits temporary copies to be made within collections. This loosening of the no assignment rule does open up a couple of coding hazards:

The first is easy to avoid and falling into it could be described as abuse:

C++
owner_ptr<T, ElementType> apMyT; //Don't do this 

The moniker ElementType is a reminder that it must only be used for the element type of a collection. Using it for named variables will produce defined but unexpected behaviour.

The second is a genuine hazard, especially if you are retro-fitting to existing code. Transfer of ownership between owner_ptr collection elements should be explicit as in...

C++
v[1] =  v[0].yield();//explicit transfer of ownership  

but you will find that this also compiles...

C++
v[1] =  v[0];   //not what you will be expecting 

its effect is defined and it will not cause memory leaks or dangling pointers but it is not intended and will not behave as you expect.

Implicit Conversion to Raw Pointers

In contrast to previous incarnations, this version of owner_ptr/ref_ptr supports implicit conversion to raw pointers. Insisting on a verbose method may be useful for marking where you have taken a raw pointer, but it means you have to use that verbose method for all calls into libraries and APIs that take raw pointers, as they do. Furthermore, those libraries and APIs are unlikely to ever conform to this paradigm, so why punish them with verbose arguments that suggest that they should. This is a big climbdown from making it idiot proof, as I originally had set out to do - a vain quest that always frustrates those that aren't idiots. It now means that you can shoot yourself in the foot as easy as this:

C++
owner_ptr<T> apT(new T);
T* pT=apT;
delete pT;  //Whoops!! - runtime error

and you can really mess things up using raw pointer casts between smart pointers:

C++
owner_ptr<T> apT(new T);
owner_ptr<T> apT2(apT);//Compiler error - not allowed
//but 
owner_ptr<T> apT2((T*)apT);//Whoops!! - two owners of the same object, double delete ahead

Just don't do it!

The advantage is that you can retro-fit these smart pointers to existing code simply by changing the pointer declarations. Everything else can be left as it is.

Returning Ownership from a Function

There is an owner_return class (Public and Private variants) that is not a smart pointer but is an owner. For the most part, this is a hidden class that is used by the yield() and yield_with_refs() dot methods to carry out transfer of ownership. The only explicit use for it is as the return type of a function from which you want to return ownership using yield() or yield_with_refs().

C++
owner_return<T> ReturnT()
{
    owner_ptr<T> apT(new T);
    //initialisation code
    return apT.yield();
}
 
owner_ptr<T> apT2(ReturnT()); // ownership transferred to apT2. Object is deleted if nothing takes ownership

Custom deleter

You can also specify a custom deleter as a third template argument in owner_ptr (Public and Private) like this:

C++
struct my_deleter
{
    template <class T>
    static inline void Delete(T* p)
    {
        p->Release();//Your alternative object deletion method
    }
};
 
owner_ptr<T, Public, my_deleter> apT;
GetObject((void**)&apT);    //COM type initialisation works 
            //if apT is not already holding anything 

<span style="color: rgb(17, 17, 17); font-family: 'Segoe UI',Arial,sans-serif; font-size: 14px;">Sidenote - The COM type initialisation </span>&apT actually returns a pointer to an owner_ptr<T> not a pointer to the object. It has to be this way otherwise it will mess up in some STL containers. The COM type initialisation works because the first class member of owner_ptr<T> is a pointer to the object and therefore has the same address. As the GetObject() function believes it is the address of a pointer, that is all it will fill so it will not overwrite anymore of the smart pointer. The smart pointer is being cheated on but it is ok as long as it was not already holding an object.

Run-time Overhead

The Private variants of owner_ptr<T> and ref_ptr<T> carry no memory overhead (being no more than the raw pointer itself) and the only code overhead beyond what would have to be written anyway is a test on dereference that throws an exception if the pointer is invalid (this defined and handle-able response to a common error has saved me a lot of trouble).

The Public variants are twice the size of a raw pointer plus a reference counter will be created for any object referenced by more than one smart pointer. The worst case memory ratio is 2.5 times a raw pointer which occurs the first time a ref_ptr<T, Public> is taken from an owner_ptr<T, Public> causing a reference counter to be created for the first time. Most operations with the Public variants cause some code to be executed but none of it is iterative or recursive, no arrays or lists are walked and reference counter adjustments do not involve any thread synchronization. N.B. In the general case, singly owned objects can only be safely referenced by the thread that owns them. Because the owning thread can destroy the object instantly (the prerogative of a single owner), it is the only thread that can be sure that it is still there.

How to Make Use of These Smart Pointers

The owner_ptr/ref_ptr with Public/Private scope visibility paradigm offers a unified approach that can provide a wide field of safe coding. That doesn't mean that you should never do anything outside of that safe field, this is C++, but it does mean you can routinely do things safely.

Keeping things wrapped with owner_ptr/ref_ptr keeps things safe and taking a raw pointer endangers them. You will still find that there are situations that are better handled with raw pointers but when they arise, you will be able to pay more attention to them.

The Public variant means that you can have lots of components holding references to each other and passing references on to each other (just like the world of humans) without this turning into a nightmare of tracking down all the pointers that need to be zeroed when you delete something. Programmers use languages with garbage collectors to write much of this sort of thing but the memory use gets squidgy, anally retentive and eventually slow.

The Private variant has zero overhead and the const nature of ref_ptr<T, Private> a wrapper for C++ reference, ensures that it cannot accidentally be used in a Public scope context.

This solution is an alternative to:

  • difficult bugs caused by dangling pointers
  • unreasonable diligence required to avoid dangling pointers
  • limitations in design, sophistication and scale due to fear of the above
  • loss of the virtues of single ownership, deterministic destruction and RAID by resorting to garbage collection due to fear of the above
  • the loss of the virtues of single ownership by resorting to inappropriate use of std::shared_ptr to avoid dangling pointers (effectively an instant response garbage collector prone to memory leaks).

When You Must Use Other Smart Pointers

Use shared_ptr and weak_ptr whenever shared ownership is your specific design intention.

Use shared_ptr and weak_ptr for any objects that may be simultaneously touched by more than one thread. In this case, you may also need protection against simultaneous access on the object that you are handling.

How Does It Work

The owner_ptr<T, Private> is a pretty straightforward exclusive ownership pointer, like std::auto_ptr, boost::scoped_ptr or std::unique_ptr. It has the specific design feature that it prohibits implicit transfer of ownership but provides a mechanism for explicit transfer of ownership. Apart from that, any one of the standard smart pointers mentioned could be used instead.

The ref_ptr<T, Private> is nothing more than a wrapper for a const owner_ptr<T, Private>&, a C++ reference to the owning smart pointer (not the object pointed at). It is the only smart pointer that can be taken from an owner_ptr<T, Private> and its const nature prevents it from having Public scope visibility.

The owner_ptr<T, Public> and ref_ptr<T, Public> pair have a more complex interaction. Both of them are twice the size of a raw pointer, containing a pointer to the object and a pointer to an autonomous reference counter if one exists. When a ref_ptr<T, Public> is set to point at an owner_ptr<T, Public>

C++
owner_ptr<T, Public> apT(new T);
ref_ptr<T, Public> rT( apT);

an autonomous reference counter is created which holds a strong count and a weak count. Its initial value is:

strong count 1 – the object is valid and has one owner

weak count 2 – the is object is referenced by 2 smart pointers

At the same time, both of the smart pointers set their pointer to reference counter to point at the newly made reference counter.

If a further ref_ptr<t,> </t,><t, /><t,>is set to point at the same object:

C++
ref_ptr<T, Public> rT2( rT);  

or:

C++
ref_ptr<T, Public> rT2(apT);

Then the weak count will increase to 3.

  • If any of the ref_ptr s are zeroed or reset, then the weak count will decrease by 1.
  • If the owner_ptr is reset, then the strong count is set to zero and the weak count is decreased by 1
  • When you attempt to test or use a ref_ptr<T, Public>, it looks at the strong count in the reference counter that it points at and if it is zero, then the ref_ptr<T, Public> will test as NULL or invalid.
  • When the weak count in the reference counter falls to zero, the reference counter destroys itself.

The owner_ptr<T, ElementType> works slightly differently. It must survive temporary copies being made in collections. To do this, it permits copy construction and assignment and interprets them as sharing ownership (increase the strong count) and destruction or going out of scope as releasing ownership (decrease the strong count). However each element must behave as a single owner, not a shared one, so resetting an element (set to NULL) forces the strong count to 0 and the object to be destroyed immediately. Thus it retains deterministic destruction and timely RAID.

The Source Code

It is just one header file with no dependencies other than the C++ language.
It is enclosed namespace xnr{ so you will need using namepace xnr; or prefix everything with xnr::

Inheritance is reserved for the only authentic 'is a' relationship; owner_ptr<T, ElementType> is a modified owner_ptr<T, Public>. Apart from that, inheritance is avoided simply because I find it annoying having the inheritance tree of a smart pointer display in the debugger and force me to drill down to get to the object I want to look at. Really, you want smart pointers to behave like language elements, you don't want them forcing you to navigate their internals. N.B. There is no inheritance relationship between Public and Private variants, their structures are different. It is partial template specialisation that allows them to be conveniently classified under the same name.

The class xng would have been a common base class had the design favoured inheritance. Instead it is a class of private static methods that can only be accessed by the Public variant smart pointers that have been declared as friends. It contains the definition of the ref_counter class and a set of loosely typed template methods that define common operations. Strict type checking is carried out in the definitions of the smart pointers that call them.

The smart pointer definitions contain many conversions declared as private so as to prohibit them. Some are necessary to prevent unwanted compiler defaults from being used and others to prevent unwanted conversions facilitated by the implicit conversion to raw pointer.

By making use of the common operations of class xng, I have been able to lay out the smart pointer definitions concisely, most of the methods fitting on one line. This is so that the overall structure can be more easily reviewed. I have tried to make it as comfortable as I can for anyone who wants to review and verify it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired
Spain Spain
Software Author with engineering, science and mathematical background.

Many years using C++ to develop responsive visualisations of fine grained dynamic information largely in the fields of public transport and supply logistics. Currently interested in what can be done to make the use of C++ cleaner, safer, and more comfortable.

Comments and Discussions

 
Questionquestion please Pin
rdnpro3-May-14 9:11
rdnpro3-May-14 9:11 
AnswerRe: question please Pin
john morrison leon4-May-14 0:01
john morrison leon4-May-14 0:01 
GeneralRe: question please Pin
rdnpro4-May-14 11:46
rdnpro4-May-14 11:46 
GeneralRe: question please Pin
john morrison leon5-May-14 2:26
john morrison leon5-May-14 2:26 
GeneralMy vote of 3 Pin
jfriedman30-Apr-14 14:27
jfriedman30-Apr-14 14:27 
Just more proof that c 9 whatever's as premature as most internet drafts.

Good verbiage.
GeneralRe: My vote of 3 Pin
john morrison leon1-May-14 0:30
john morrison leon1-May-14 0:30 
GeneralRe: My vote of 3 Pin
jfriedman5-May-14 5:55
jfriedman5-May-14 5:55 
GeneralRe: My vote of 3 Pin
john morrison leon5-May-14 8:58
john morrison leon5-May-14 8:58 
GeneralRe: My vote of 3 Pin
jfriedman5-May-14 9:31
jfriedman5-May-14 9:31 
GeneralRe: My vote of 3 Pin
john morrison leon5-May-14 10:11
john morrison leon5-May-14 10:11 
GeneralRe: My vote of 3 Pin
jfriedman6-May-14 9:06
jfriedman6-May-14 9:06 
GeneralRe: My vote of 3 Pin
john morrison leon6-May-14 9:27
john morrison leon6-May-14 9:27 
GeneralMy vote of 5 Pin
sprice8631-Mar-14 0:40
professionalsprice8631-Mar-14 0:40 
GeneralRe: My vote of 5 Pin
john morrison leon2-Apr-14 5:36
john morrison leon2-Apr-14 5:36 
QuestionIssues with images and download Pin
Mike Hankey26-Mar-14 4:29
mveMike Hankey26-Mar-14 4:29 
AnswerRe: Issues with images and download Pin
john morrison leon26-Mar-14 4:50
john morrison leon26-Mar-14 4:50 
GeneralRe: Issues with images and download Pin
Mike Hankey26-Mar-14 5:00
mveMike Hankey26-Mar-14 5:00 
GeneralRe: Issues with images and download Pin
john morrison leon26-Mar-14 5:13
john morrison leon26-Mar-14 5:13 
GeneralRe: Issues with images and download Pin
john morrison leon2-Apr-14 5:38
john morrison leon2-Apr-14 5:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.