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:
- 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)
- 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:
- owner_ptr<T> the exclusive owner
- 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.
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.
- 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).
- 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.
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>
- 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:
owner_ptr<T> apT(new T); if(apT) apT->DoSomething(); apT=NULL;
ref_ptr<T> rT( apT); if(rT) rT->DoSomething(); rT=NULL;
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:
owner_ptr<T> apT(new T);
owner_ptr<T> apT2( apT);
and any attempt to point a ref_prt<T>
at something not yet owned by an owner_ptr<T>
ref_ptr<T> rT(new T);
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.
owner_ptr<T> apT(new T);
owner_ptr<T> apT2((apT.yield());
The swap(...) dot method will swap two owners of identical types.
owner_ptr<T> apT(new T);
owner_ptr<T> apT2.swap(apT);
The Public Variants
The Public variants are needed wherever a ref_ptr
has a wider scope than the owner_ptr
it references. Most trivially:
ref_ptr<T, Public> rT;
{
owner_ptr<T, Public> apT(newT);
rT=apT; rT->DoSomething();
}
if(rT) rT->DoSomething();
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:
-
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());
apT2.swap_with_refs( apT);
-
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;
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>
.
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:
owner_ptr<T, ElementType> apMyT;
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...
v[1] = v[0].yield();
but you will find that this also compiles...
v[1] = v[0];
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:
owner_ptr<T> apT(new T);
T* pT=apT;
delete pT;
and you can really mess things up using raw pointer casts between smart pointers:
owner_ptr<T> apT(new T);
owner_ptr<T> apT2(apT);owner_ptr<T> apT2((T*)apT);
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()
.
owner_return<T> ReturnT()
{
owner_ptr<T> apT(new T);
return apT.yield();
}
owner_ptr<T> apT2(ReturnT());
Custom deleter
You can also specify a custom deleter as a third template argument in owner_ptr
(Public
and Private
) like this:
struct my_deleter
{
template <class T>
static inline void Delete(T* p)
{
p->Release(); }
};
owner_ptr<T, Public, my_deleter> apT;
GetObject((void**)&apT);
<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>
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:
ref_ptr<T, Public> rT2( rT);
or:
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.