This article has been superceeded by:
A Sensible Smart Pointer Wrap for Most of Your Code
Please use the more mature version presented in the new article
Here is a smart pointer system suitable for C++ application development, in which:
- You write less object and reference management code than in Java or C#.
- You lose none of the precise and deterministic control of object lifetimes that C++ provides.
- You do not suffer from memory leaks or dangling pointers.
- You do not suffer from unintended failure to release references.
- Although there is a small memory and execution overhead, its operation is direct and without iterations.
It adheres to the principle that object lifetimes and memory use should be deterministically controlled by the scope and the programmer's explicit design, and should not be compromised by unreleased secondary references as occurs with the practice of using
boost::shared_ptr as a 'catch all' solution.
It is accompanied by an example project which demonstrates and tests many aspects of the smart pointer system. It is a Microsoft Visual Studio 2005 project using ATL/WTL. The smart pointer system itself is pure C++, but this implementation makes use of ATL collection classes to provide arrays and lists.
This article and the associated source code have been updated on 4 Feb 2009 to include support for multithreading.
The following sections have been added to the text of the article:
- Complexities introduced by multithreading
- Design decisions affecting multithreading
- How multithreading support works
- Limitations of multithreading support
A more complete description of the changes is given in the revision history.
There has been, for many years, a well established model of C++ programming in which all objects dynamically created with the
new operator are assigned to a smart pointer which ensures that the object is deleted automatically at the appropriate time - which is when the smart pointer is reset or goes out of scope. This model is good for eliminating memory leaks, and also eliminates the need to write code that calls the delete operator. These smart pointers can be described as exclusive ownership pointers. Examples are:
This works very well, introduces no complications, and is a thoroughly recommended practice, but it still leaves a large problem unsolved which sends shivers up the spine of many developers and, more importantly their bosses, - the 'dangling pointer':
All of the programs I have written have also required me to use pointers which don't own the objects they point at, they just reference them as part of the operation of the program, e.g., a pointer to the current focus. For these, I have always used raw pointers because that is all there are. The problem is that these secondary non owning references can very easily become dangling pointers. Being just plain raw pointers, they don't know when the object they point at has been deleted. Code has to be written to ensure that when an object is deleted, all pointers that reference it are set to
NULL first. This code can be complex, repetitive, ugly, tedious to write, and therefore very easily faulty.
There is no doubt that the highly skilled (and therefore expensive) man hours that are lost ensuring that pointers don't get left dangling, or even worse trying to find out why an application crashes inexplicably, have led to the popularity of languages that rely on garbage collection to manage object lifetimes, such as Java and C#.
My feeling is that if programmers choose Java or C# rather than C++ because they are more convenient for certain tasks, then that is fine, but if they are frightened to touch C++ for fear of dangling pointers, then let's fix the problem in C++.
There are garbage collectors for C++, but really, they turn it into another language. For one thing, they will play havoc with many class libraries that encapsulate the releasing of resources in destructors.
The solution that seems to be gaining acceptance for C++ is to wrap all pointers as strong or shared pointers so that objects are only deleted when the last thing pointing at them is reset or goes out of scope. This provides the same guarantee that objects will stay alive as long as something points at them, but instead of waiting for a garbage collector, they are deleted promptly as soon as nothing points at them. This is the solution recommended by C++ Standard Library TR1 extension for memory handling which has adopted the
boost::shared_ptr. This worries me because it has two drawbacks that are not good for the health of C++ development:
- One is widely acknowledged - the problem of cyclic references. If you use
shared_ptr for all secondary references, then when two objects directly or indirectly refer to each other, they will never be released – each keeps the other alive forever. This is a classic permanent memory leak caused by using
shared_ptr. The recommended solution is to identify the cyclic reference situation and break the cycle with a
weak_ptr. The fact that a weak pointer is needed to break a cycle of strong references simply illustrates how inappropriate it is to use strong pointers for these roles in the first place. Cyclic references are not always easy to identify, so this represents a considerable programming hazard that
- The other problem is hardly ever mentioned, although it is equivalent in the world of garbage collection, has now become an issue, and has a name "failure to release secondary references". Using
shared_ptr to wrap all pointers does eliminate the possibility of pointers dangling, but it does so by extending the lifetime of the objects they point at. This is OK if it is what you want, but if it was simply an oversight not to
null the secondary reference, then you will be retaining the object it points at in memory for no good reason – probably in conflict with your design intentions.
With both of these problems, neither the compiler nor the run-time system has any way of knowing that you have made a mistake, so you don't even get a warning – you will not know that your design intentions have been compromised until serious memory problems emerge further down the line.
What is being missed here is that the problem in C++ never was objects being deleted too early, it was secondary pointers continuing to point at them when they are not there because of an oversight of the programmer. So, why are we trying to cover up the error of the programmer by making the objects live longer?
To put this simply:
- To extend the lifetime of an object (which was probably correctly designed) to appease a non nulled secondary pointer (which was an oversight) is plain wrong.
- Don't extend the life of the object to appease the erroneous pointer – tell the erroneous pointer that the object has been deleted.
I worry that the very action of making all pointers
shared_ptrs will lead to sloppy programming, and that the fact that this model specifically hides the consequences of this sloppiness is going to degrade the quality of C++ development.
While considering all of this, I found myself thinking "all I want is that my secondary references automatically test as
null if the object they were pointing at has been deleted".
The system presented here is the result of doing just one thing: having a smart pointer for non-owning reference pointers which includes a mechanism to automatically set them to
NULL when the object they point at is destroyed. The mechanism necessarily has to have the owner of the object involved, therefore a new type of owner pointer is required to work with it. For this reason, I have called it:
XONOR pointers = eXclusive Ownership & Non Owning Reference pointers.
This is a smart pointer system with a mechanism to eliminate both memory leaks and dangling pointers which respects the distinction between exclusive ownership pointers and non-owning reference pointers. A smart pointer system offers the opportunity for a much needed declarative distinction between the two - and here it is:
owner_ptr<T> is an exclusive ownership pointer to an object of type
It is the primary reference to a dynamically allocated object and the controller of its lifetime. Effectively, you can think of it as being the object itself (it is as close as you can get to it!).
ref_ptr<T> is a non owning reference to any object of type
T whose lifetime is already controlled by an
This is a weak pointer, but unlike the second class
weak_prt<T> (TR1), it is a first class pointer that can be de-referenced and used in the same way as a raw pointer. You use it everywhere that you want to hold a reference to something that already exists. It is named simply
ref_ptr to indicate that it is just an observing reference, and the name is brief to encourage its frequent and almost casual use.
With this distinction made, it is possible to construct a system which handles memory correctly with no anomalies and no new programming hazards.
In addition, a shared ownership pointer is provided for genuine cases of shared ownership (not for holding secondary references!!).
strong_ptr<T> is a shared ownership pointer.
Sometimes, you really do want a shared ownership pointer; for instance, to control the life of a resource or service with widely distributed use.
strong_ptr is provided for this.
strong_ptr survives the storage mechanisms of STL collections that read elements out by making temporary copies. With
owner_ptr<T> (or any exclusive ownership pointer), this 'copying out' process has the undesirable effect of causing the pointee to be deleted.
Building a Complete Integrated Solution
The aim of this system is to provide C++ programmers and their managers with reasonable confidence that their code will not generate memory leaks or suffer from dangling pointers. Therefore, it must embrace as much as possible of what they are likely to do. Ideally, it should be possible to code without declaring or using any raw pointers. This idea cannot be fully achieved because most APIs work with raw pointers, but what can be achieved is that the use of raw pointers is considerably reduced and made explicit so that any problems they cause can be quickly found. With this aim, the following has been added:
Sometimes, some work needs to be done on a new object before assigning it to a permanent owner - this may even include deciding what is to be the permanent owner or owners:
new_ptr<T> can temporarily hold an object created with the
new operator before assigning it to an
strong_ptr. This is useful for initialising a new object before adding it to a collection.
If the new object is not assigned to an owner,
new_ptr will delete it when it goes out of scope.
new_ptr can be used as the return value of an object creation function. It has the advantage that if the return value is not assigned to an owner, then the object will be deleted automatically when the function returns.
new_ptr has been assigned to an owner, a
ref_ptr can be taken from it which will point at the new owner.
You cannot take a secondary reference (
ref_ptr) from a
new_ptr until it has been assigned to an owner. If you attempt to, the secondary reference (
ref_ptr) will simply hold
NULL even though the
new_ptr has a value.
The same initialisation work can be done using
strong_ptr as both can be returned from functions; however,
new_ptr<T> has three advantages:
- It provides a declarative indication within a function or code block that its purpose is the initialisation of a new object before assigning it to an owner.
- It allows creation functions to be created that work for both
strong_ptr (conversion between
strong_ptr simply cannot be allowed).
- It allows a slightly more concise and tidier initialisation sequence in many cases. as shown in the code samples below:
ref_ptr<CRectangleBox> AddRectangleBox(CPoint point, CSize size)
owner_ptr<CRectangleBox> newRectangleBox(new CRectangleBox);
ref_ptr<CRectangleBox> AddRectangleBox(CPoint point, CSize size)
new_ptr<CRectangleBox> newRectangleBox(new CRectangleBox);
The difference is that with
owner_ptr (as with
std::auto_ptr), assignment is necessarily destructive. This means that the assignee takes ownership and the assigner is set to
NULL - this has to be the case because two pointers can't both be exclusive owners.
new_ptr behaves differently; instead of setting to
NULL when it is assigned to an owner, it becomes a passive non-owning observer (for the rest of its short lifetime). References can then be taken which will point to the new owner.
It is not necessary to use
new_ptr<T>, but its use is recommended.
This is a variant of
ref_ptr has a bit of execution overhead each time you deference it - it checks that the object is still there first (that is what it is for). If you have a code block that makes extensive use of a
ref_ptr, de-referencing it many times, then it is a bit obsessive and wasteful of execution time to check that the object exists each time.
fast_ptr behaves slightly differently - it doesn't check that the object exists at all when it is de-referenced; instead, it locks the object into existence when the
fast_ptr is initialised, and unlocks when it goes out of scope. This doesn't mean it keeps the object alive, it means that any attempt to delete the object while the
fast_ptr is using it will cause an exception to be thrown.
fast_ptr only within code blocks within which you can be reasonably confident that nothing is going to try and delete the object it is referencing. A
fast_ptr can never be
null. It must be initialised on construction with a valid non null reference, and cannot be reset. The only way to use it is as follows:
It is not necessary to use
fast_ptr<T>, but its use can speed up the execution of critical code blocks.
enable_ref_ptr_to_this<T> class and its
Sometimes it is necessary to take a reference to the
this pointer from within a class.
enable_ref_to_this<T> is an add-in base class which you can add to the inheritance list of the class that you are working with. It provides the
ref_to_this() method which returns a
ref_ptr wrapping the
enable_self_delete<T> class and its
Some objects delete themselves: for instance, modeless dialogs.
enable_self_delete<T> is an add-in base class which you can add to the inheritance list of the class that you are working with. It must be initialised by an
owner_ptr, either on construction, or by calling its
set_owner() method. The object should delete itself by calling the
self_delete() method. The
self_delete method ensures that the
owner_ptr is nulled as the object is deleted. In this case, the
owner_ptr must not be stored in a dynamic array or any kind of container that moves its elements around in memory.
referencable(Type, value variable name) is a macro for declaring objects by value so that a
ref_ptr can be taken from them. Sometimes, we have a class member declared by value because there is no reason to declare a pointer and create the object with the
new operator, but we still want to be able to take a safe
ref_ptr reference from it.
ref_ptr_to(value variable name) is a macro for taking a
ref_ptr from an object declared by value using the
Collections of Xonor Pointers
Some care is needed with storing
owner_ptr<T> in collections. It seems that all collections make temporary copies of their elements as they are added, and
owner_ptr<T> is OK with this (due to its destructive copy constructor). This means that you can use
owner_ptr<T> in ATL collections such as
For ATL, the optional XonorAtlColls.h defines
owner_ptr_list<T> as follows:
template <class T> class owner_ptr_array : public CAtlArray<owner_ptr<T> >
template <class T> class owner_ptr_list : public CAtlList<owner_ptr<T> >
Some collections (in particular, STL (Standard Template Library) collections) also make temporary copies of their elements as they are read out and while performing various internal operations.
owner_ptr<T> is not ok with this - it causes the elements involved to delete their objects. You cannot directly use any kind of exclusive ownership pointer in STL collections - they may become unintentionally deleted. For STL collections, you must use
strong_ptr<T> for elements that own their object.
strong_ptr<T>, as its name suggests, is very robust, and will survive just about anything. Of course, if you do this, you lose the declarative guarantee of exclusive ownership, but if you are careful not to share these elements with other
strong_ptr<T>s, they will still behave as exclusive owners.
For STL, the optional XonorSTLColls.h defines
owner_ptr_list<T> as follows:
template <class T> class owner_ptr_array : public vector<strong_ptr<T> >
template <class T> class owner_ptr_list : public list<strong_ptr<T> >
If you really want to have an STL collection that holds
owner_ptr<T>s (because you want a declarative assurance that they will not be shared), you can use
owner_ptr<T, false> (which doesn't delete its pointee when it goes out of scope) for the collection elements, and override the collection class to handle every occurrence that involves removing elements from the collection so that
reset() is called on the
owner_ptr. This is a bit of work, but results in a solidly engineered generic collection which does exactly what you want.
There is no problem whatsoever with storing
ref_ptr in any kind of collection, and
fast_ptr just doesn't belong in collections.
Rules of Use
The following operations are the same for all smart pointers in this system, except that
fast_ptr cannot be initialised as
|dereference operator |
|initialise as |
owner_ptr<CObject> opObject;<br />owner_ptr<CObject> opObject(NULL);<br />owner_ptr<CObject> opObject=NULL;
|All pointers except |
|test for non |
|All pointers except |
|test for |
|All pointers except |
Comparisons test if two pointers point at the same object. In the case of comparing two
owner_ptrs or an
owner_ptr with a
strong_ptr, it will always return
owner_ptrs and also an
owner_ptr and a
strong_ptr cannot point at (and therefore own) the same object; therefore, by definition, the comparison must return
new_ptr does not support comparison.
|Tests that both pointers point at the same object|
|Always returns |
|Does not support comparison|
The rules of assignment and construction are what really determines the shape of this system, because they determine how each kind of pointer gets to hold a non null reference.
|assignment and construction|
|an uncast raw pointer returned by the |
|an uncast raw pointer returned by the |
new_ptr which then becomes a passive observer from which references can be taken
owner_ptr which will lose ownership and be reset to
ref_ptr & fast_ptr
fast_ptr construction only)
new_ptr that has been assigned to an owner
|an uncast raw pointer returned by the |
new_ptr which then becomes a passive observer from which references can be taken
NULL if not a reference to another
= operator of
owner_ptr applied to another
owner_ptr performs destructive assignment in the same way as
std:auto_ptr. It transfers ownership and leaves the source pointer empty. This operation is not intended to be used in application code, but has to be there so that
owner_ptr can be returned from functions. Destructive assignment is anti-intuitive (you would expect
= to make a copy and leave the source unchanged); therefore, it is not helpful for it to appear in application code. Instead, use one of the two dot methods which make it clear what is being done:
.make_copy(owner_ptr<T>) makes a value copy of the object owned by the other
.steal_object(owner_ptr<T>) takes the other
owner_ptr object and makes it its own. The other
owner_ptr is left as
NULL. This is the same effect the
= operator would have.
owner_ptr has a
public destructive copy constructor which is needed so it can be used in collections. Although it is
public, you should not use it in application code - in this case, it is hard to see why you should want to!
Polymorphism is fully supported. Automatic implicit downcasts from derived to base classes are provided for all assignments.
With a polymorphic system, it is normally important that the base class destructor is declared as
virtual - this ensures that the
delete operator always deletes an object of the derived class that was created using the
new operator - otherwise, only the base class portion will be deleted, leaving a memory leak (class slicing). It is very easy to forget to do this! With this smart pointer system, this is unnecessary - the smart pointer system knows which derived class was used to create the object, and ensures that an object of the same class is deleted. It is very important not to cast an object explicitly before or during its assignment to an owner.
up_cast<T, U>() function is provided to cast all types from base to derived types where this is necessary. Otherwise, there should be no explicit casting.
What Can Still Bite You
The points of vulnerability with this system are its interfaces with raw pointers.
new_ptr are initialised by a raw pointer returned by the
new operator. The pointer returned by the
new operator should be immediately assigned to one of these smart pointers without being changed in any way and then should never be used again.
owner_ptr<U> op=new T;
It is not hard to see that the following would break the system:
T* pT=new T;
owner_ptr<U> op1= pT;
owner_ptr<U> op2= pT;
but this one is not so obvious:
owner_ptr<U> op=(U*)new T;
It may look like the cast
(U*) is harmless or even helpful, but it conceals the original type
T from the initialisation of the
owner_ptr. This line of code will prevent the anti class slicing mechanism from working, resulting in memory leaks. The
owner_ptr has its own implicit casting operator, but it will only act correctly if it is assigned with the original uncast pointer returned by the
To avoid such problems:
- Never even name a variable to hold the pointer returned by the
new operator - always assign it directly to a XONOR pointer.
- Never cast the pointer returned by the
new operator - not even during the assignment to a XONOR pointer.
If you need to carry out initialisation on a new object before giving it an owner, then assign the pointer returned by the
new operator directly to a
new_ptr which can hold it temporarily before it is assigned to an owner.
- Many APIs take raw pointers as parameters. In general, this is fine - you can pass in the raw pointer of any XONOR pointer by using the dot method
.get_pointer(). There are two things an API can do with these pointers which would break the system:
- Delete the pointer you have passed - the smart pointer system will know nothing of this, and will try to delete it again later.
- Store the pointer you have passed - the stored pointer will not know when you delete the object and could dangle.
Most APIs will not do these things, and if they do, it will usually be made clear in the documentation.
- Many APIs have return values which are raw pointers - here, you should take a great deal of care. You need to know if you are being given a secondary non owning reference pointer (which it will be in most cases), or if it is a pointer that you have to take ownership of (if this is the case, then the documentation should say so quite clearly).
If you have to own the object, then assign the return value directly to an
new_ptr as if it was the return value from the new operator.
If (as in most cases) it is a non owning reference pointer, then use it and store it as a raw pointer. I'll say that again - use it and store it as a raw pointer. It is futile to try and hold it with a XONOR pointer because we know nothing about its lifetime. Handle this raw pointer with care - this is something between you and the API that you are calling.
- XONOR pointers can throw two types of exceptions:
xonor_ptr_fatal_exception. Both types of exceptions are only thrown because of coding errors which you should correct. No external factors, not even memory or resource allocation will throw
xonor_ptr_exceptions. It is not really a good idea to handle either type and continue execution, but if you insist on covering up mistakes in this way, then you can handle a
xonor_ptr_exception and continue execution, but never handle a
xonor_ptr_fatal_exception and continue execution - these fatal exceptions are typically thrown in constructors and destructors, and therefore damage has already been done. If a
xonor_ptr_fatal_exception occurs, you have made a serious coding error and you must correct it.
- The destructive copy constructor and assignment operator of
owner_ptr applied to another
owner_ptr has already been mentioned. Misunderstanding this won't provoke memory leaks or dangling pointers, but it may surprise you.
enable_self_delete add-in base class is, strictly speaking, not part of the XONOR pointers system; it is a utility that works with it, and it has the capacity to break it.
enable_self_delete uses the address of an
owner_ptr, so it is essential that the
owner_ptr must not move. This means that it must not be stored in a dynamic array. Passing an
owner_ptr stored in a dynamic array to
enable_self_delete can result in the worst type of memory corruption that we have gone to so much trouble to avoid. So far, I haven't found a way of detecting and preventing this, so it is best to regard
enable_self_delete as potentially dangerous but sometimes very useful. Taking the address of an
owner_ptr is, in general, prevented, but
enable_self_delete has a special permission to do this, but is unable to detect when it is inappropriate.
How it Works
For the most part, these smart pointers are similar to others, implemented as template classes, with a great deal of operator overriding, and making use of destructors to either delete the pointee or reduce its reference count. What is different is the integration of several smart pointers with different roles to work together.
All smart pointers hold a pointer to an object of type
T, and also a pointer to a reference controller.
- A weak reference count.
- A strong reference count.
- A pointer to the original object and of the same type as the original object, and also has a virtual function that is called to delete objects.
A reference controller is created whenever two or more
xonor_ptrs point at the same object. It is also created on construction and assignment from the
new operator when the stored type is a base class of the type created because its virtual function is required for the anti class slicing mechanism.
The strong reference count indicates the number of
xonor_ptrs owning the object. This can be:
- 1 -
owner_ptr or a single
- many - multiple
- or 0 - the object has been deleted
The strong reference count controls the object lifetime - when it goes to zero, the object is deleted.
The weak reference count indicates the number of
xonor_ptrs referencing the object. This can be:
- 1 or many - there is still a reference to this object
- or 0 - nothing references the object anymore
The weak reference count controls the lifetime of itself. When the count goes to zero, it is deleted.
The reference controller associated with a
XonorPtr may not be specialised to the same type as the
XonorPtr itself. This is because the reference controller must hold (and delete) a pointer of the same type as the original object created. The
XonorPtr doesn't know the type specialisation of its reference controller. It only knows it as an unspecialised base class - for this reason, it calls a virtual function of the reference controller when it is time to delete the object.
It does appear that there is some redundancy in holding the pointer to the created object. There is a copy in each
XonorPtr and also in the reference controller. I think the redundancy is real, one copy should be enough if we could have a system that casts it appropriately depending on where it is used. I think that this could be done, but my efforts, although partly successful, produced too much obfuscation, which I particularly wanted to avoid. So for now, the redundancy remains in the interest of having solid transparent guarantees of correct operation.
Now, let's look at the "user forgets to
null a reference" scenario in the simplest way:
owner_ptr<T> opT=new T;
In the first line, the internal pointer of
opT is assigned by
new T - no reference controller is created. In the second line, the internal pointer of
rT is assigned by the internal pointer of
opT, and a reference controller is created with a strong count of 1 and a weak count of 2. In the third line,
rT checks that the strong count is non zero and
DoSomething() is called. In the fourth line,
opT is reset. The object is destroyed, and its internal pointer is set to
NULL. Also, the strong count is set to zero and the weak count is reduced to 1. In the fifth line,
rT is tested for being non-null, and returns
false because the strong count is zero. The pointer knows now that the object is gone, and therefore resets itself and releases the reference controller, reducing its reference count to zero which causes the reference controller to be deleted. The sixth line does not get called.
If the test for non null had been omitted, then the dereference operator would have made the same check on the strong count and would have thrown an exception.
The key functionality is that the
reference_controler can outlive the object it is associated with, and continues to exist so that any remaining secondary references are able to see that the object no longer exists.
strong_ptr has the same interaction with the
WeakCount, but additionally increments the
StrongCount on taking a reference, and decrements it when it goes out of scope or is reset.
- When the
StrongCount decrements to zero, the object pointed at is deleted. The life of the object is over.
owner_ptr can simply delete the object. If it has a
StrongCount will be set to zero. The
reference_controler itself will only be deleted if the
owner_ptr was the last thing pointing at it.
- When the
WeakCount decrements to zero, the
reference_controler itself is deleted. There are no more secondary references that need to know about it.
There are two further complications:
If an object is held by a
strong_ptr, then its weak count is held as a negative number. This is to make it possible to assign
strong_ptr from a
ref_ptr if the object it references has shared ownership
strong_ptr(s). This is correct and useful. A negative weak count indicates that it references an object with shared ownership, and therefore this can be done. Otherwise, the object is exclusively owned, and this cannot be done - the
strong_ptr will be simply assigned
fast_ptr uses a negative
StrongCount to lock the object against destruction. If the
StrongCount is negative when an attempt is made to delete the object (for instance, its primary reference goes out of scope), then an exception is thrown.
A side effect is that if a
fast_ptr is taken from a
strong_ptr, then the
StrongCount is already in use with a positive value, which produces a conflict. The solution is that the
fast_ptr abandons the idea of locking, and increments the
StrongCount, thus acting as another
strong_ptr. This isn't really a problem because already the object is no longer directly controlled by scope, and as
fast_ptr normally has a short lifetime, it is unlikely to unnaturally extend the life of the object. In any case, if the
fast_ptr is the last reference left keeping a shared object alive when it goes out of scope, it will throw an exception.
One thing that concerned me most was the allocation of
reference_controlers. Initially, I had them being allocated directly from the heap using the
new operator, and then I realised two undesirable consequences:
- Frequent heap allocation of small objects is costly.
reference_controler which remains in memory due to live references to deleted objects will tend to cause fragmentation, making new memory allocations more complicated.
The solution to this was to create a pool from which the
reference_controlers are allocated. A pool of 100 is created on startup, and if that is exhausted, then it is extended by a further block which is 10% larger, up to a maximum of 100 blocks. This mildly exponential growth rate doesn't increase the granularity of block allocations too dramatically, but allows a maximum of 13 million reference controllers.
The idea is that this:
is better than this:
Although the first has 100 slots reserved for reference controllers and the second only occupies the space needed by the 4 live
reference_controlers, the second case is more problematic for future memory allocations.
The reference controller pool always knows where the next free slot is (easy to achieve because they are all the same size), so allocation and deallocation is very fast.
Because the reference controller pool is global, its methods for allocation and de-allocation are protected by a single critical section so that using XONOR pointers in more than one thread doesn't cause thread conflicts. As indicated below, that does not mean that these smart pointers are thread safe - they are not!
Transparency of Source Code
I have completely revised the layout of the source code implementing these smart pointers to try and make their operation as transparent as possible.
Each smart pointer class has a small number of
protected methods that carry out specific actions, each with a verb like
name. These are laid out as I normally lay out code - a new line for everything, indentation, and lots of white space.
Having different types of smart pointers interacting with each other means that there are a large number of
public methods representing the possible conversions between them - mostly constructors and overriding of the assignment operator. These are laid out as one-liners, making a simple sequence of calls to the verb, like
protected methods. This makes it easier to get an overview of how they work, check them for consistency, and verify their correctness.
I have done this very specifically in the hope that it will encourage programmers to try it and consider adopting it. To trust this system, it is not enough to agree that it is a good idea, you also have to be able to see that it has been done right!
Overhead and Performance
Yes - these is an overhead with this system:
- In general, XONOR pointers occupy up to three times more space than if raw pointers were used.
- Assignment of one XONOR pointer to another involves the execution of several lines of code.
- Dereferencing of
ref_ptrs involves an extra step of checking the strong ref count.
- Dereferencing of an
strong_ptr only involves a non null check of its own internal pointer.
- Dereferencing of a
fast_ptr is instantaneous - as if it is a raw pointer.
- None of the execution overhead involves any iterations or walking of collections.
- A reference controller pool avoids continual construction and destruction of reference controllers on the heap.
Complexities Introduced by Multithreading
Whilst all operations on an object are in one thread, we are able to make assumptions that simplify things. For instance, we can allow a
ref_ptr to be directly de-referenced. This is because we can be sure that in the following code:
rp->DoFunction();, nothing else can change
If the object is also referenced in another thread, then we cannot rely on this. It is possible that between
rp->DoFunction();, the other thread may have invalidated
Also, while in a single thread, the reference counting can be implemented as simple increment
++ and decrement
--. If the object is also referenced in another thread, it is possible that both threads may try to change the reference count at the same time, and we will need to use
InterlockedDecrement, which carry more overhead.
Design Decisions Affecting Multithreading
- Only an object with shared ownership can be shared between threads.
- A thread may hold a
shared_ptr or a
ref_ptr to an object in another thread.
ref_ptr to an object in another thread must not be de-referenced directly; it must be converted to a
strong_ptr before it is used, and the
strong_ptr should then be tested before use.
We will examine these in reverse:
- #3. The idea of a
ref_ptr is that it doesn't keep the object it points at alive. So, if we are holding a
ref_ptr to an object in another thread and it gets deleted, then when we go to use it, we test it first and we see it is
NULL, and that is perfectly correct. The problem is if we test it and find it is valid, and then immediately afterwards and before we can execute the next line of code, it gets deleted by the other thread. The only way to make sure that this doesn't happen is by owning the object and keeping it alive while we use it. This is why we have to convert it to a
Furthermore, if we test it first and then convert it – it could get deleted by another thread between the test and the conversion. Therefore, it is necessary that the test and incrementing the strong reference count are one and the same operation: Conversion from
strong_ptr has been modified to achieve this. The
strong_ptr keeps the object alive as long as that
strong_ptr is valid. If the object was not valid, then the
strong_ptr will test as
Lock() method has also been added to
ref_ptr to do exactly the same thing, but more explicitly.
Lock() returns a
strong_ptr with which you can work after testing that it is not
- #2. There are times when we want to hold a pointer to an object in another thread and we want that object to be kept alive by the pointer – in this case, we hold a
strong_ptr to the object in the other thread. There are other times when we just want to hold a pointer to the object as long as the object exists. In this case, we use a
ref_ptr; if the
strong_ptr that we convert it to tests as
NULL, then we know the object is no longer there. If the
strong_ptr is valid, then we know it was still alive and that we will now keep it alive as long as the
strong_ptr is valid.
- #1. An object whose lifetime is controlled by single ownership cannot be shared between threads. This is because the single owner can destroy the object unconditionally, and a user in another thread can do nothing to stop this. I did think about allowing a temporary relaxation of single ownership (the other thread can keep the object alive for just a short time each time it works with the pointer), but I decided that if you declare single ownership, then you want deterministic destruction. You want your destructors to be called, and you want them called in the correct sequence (usually well controlled by scope). The problem is that if you keep a singly owned object alive even for a short time, it can delay the calling of its destructor and cause it to be called out of sequence. We don't want to mess with this!
So, we have two ways of sharing a pointer to an object across threads:
strong_ptr<T> opT2= opT;
ref_ptr<T> rT= opT;
strong_ptr<T> opT2= rT;
It is important to restate the rules negatively – what you must not do.
- Pass any kind of pointer to an
owner_ptr to another thread.
owner_ptr is strictly single thread.
- Do anything with a
ref_ptr to an object in another thread other than convert it to a
strong_ptr or set it to
NULL. Setting a
NULL is simply telling it not to take any further interest in what it is pointing at, this is never a problem.
Understanding what you must not do is important because there is no way of enforcing these rules without introducing an unacceptable overhead. I think it is very important that a
ref_ptr can be directly de-referenced within a single thread, and it does not make sense to prohibit this just to prevent its misuse with multiple threads.
The following exemplify wrong use:
ref_ptr<T> rT= opT;
ref_ptr<T> rT= opT;
How Multithreading Support Works
First of all, we are already carrying an indication in the reference controller of shared ownership – if it is shared, then the weak reference count is held as a negative number. As shared ownership objects can be shared across threads, all changes to the reference counts are made using
InterlockedDecrement if the weak reference count tests as negative.
The second issue is that testing and incrementing the strong reference count should be one and the same thing during conversion from
All conversions from
strong_ptr call an internal method
strong_ptr, and it is here that we make the necessary changes.
inline void ShareOwnership(ref_ptr<T> const& spT)
First, we test to see if there is a reference controller. The
m_pReferenceControler pointer will either be
NULL (we never pointed it at anything), or it will be valid for as long as anything points at it – it cannot be deleted from under us by another thread – no worries here.
Next is the first important trick. We want to know if the object has shared ownership (it is illegal to construct a valid
strong_ptr to a singly owned object). A negative weak reference count tells us this, but we don't want to be reading it while another thread is changing its value (we may pick up an intermediate value which is gobledegook). So, we call
InterlockedDecrement on it and read the return value (guaranteed to be meaningful). If this is negative, then we have shared ownership, and can go ahead (remembering that we have already increased the weak count). Otherwise, we have single ownership, and the
strong_ptr should be left as
null, and we call
InterlockedIncrement on the weak count to restore it.
We now do a similar trick with the strong reference count. Remember that if we test it before we increment it, we risk it being deleted between one operation and another. So, we just go ahead and call
InterlockedIncrement unconditionally without even knowing if the object was valid (we always know the reference controller is valid). If the return value of
InterlockedIncrement is greater than
1, then we know that there was an object, and we go ahead and make the conversion to a valid object. However, if the return value of
InterlockedIncrement is only
1, then we know that there was no object there – its previous strong count was
0 – so we call
InterlockedDecrement to restore its value, and we do no more, and leave the
This care is only needed with the conversion from
strong_ptr because it is only with
ref_ptr that the object that it points at can be deleted unexpectedly.
Limitations of Multithreading Support
This multithreading support, used correctly, provides guarantees that there will not be conflicts between threads over the existence of an object and the validity of pointers to it. This is all a smart pointer system can do on its own.
It does not do anything to stop two threads from trying to modify an object at the same time. Any object shared between threads needs to have its own protection against threads clashing as they try to modify the same data.
It does not provide an integrated solution to sharing data between threads – this is a complex issue! It just guarantees that using these pointers doesn’t undermine whatever solution you create.
Using the Source Code
Copy all of the XonorPtrs header files (all the header files that begin with 'Xonor', found in the XonorPtrs folder) into an include directory, and include the following in the file you want to build:
using namespace XonorPtrs;
If you compile more than one *.cpp file, then you should only include XonorPtrs.hpp in one of them.
To use it, just start declaring your pointers as
XonorPtrss, and code as normal. If you stay with it, you can enjoy the luxury of not having track down and
NULL multitudes of secondary references.
This system has been designed so that normal object orientated programming is more comfortable and less hazardous, and I have tried to embrace as much as possible of the kind of scenarios that I have found in my programming experience.
- If you try strange and unconventional constructions, then it may not work - I have tried to prohibit unusual use by generating compiler errors, but I doubt that this is watertight.
- It is not 'thread safe'. It does not provide a mechanism for the smart pointers to be simultaneously accessible to two threads.
One of the problems with developing a smart pointer system is that if it does not work properly, it will cause the very problems it has been designed to solve. What I present here works, and has not failed any test I have put it through, but the code implementing it is sufficiently lengthy and complex to have some chance of containing errors or oversights that have not been detected. I hope that by making the code transparent, I have made it easier to spot and correct any mistakes, and that this has reduced the risks involved with adopting it.
The Example Program
The example program is a simple diagram builder/editor. You can create rounded rectangles and ellipses, you can move and size them graphically, you can join them together with lines, fill them with text, and select an individual font and text colour for each box.
It has been written specifically to demonstrate and test the use of XONOR pointers. In most cases, I have tried to come up with plausible applications of the functionality of XONOR pointers.
There are two classic polymorphic subsystems:
- Visual Objects, which is very concretely object orientated.
- Mouse Modes. which is a more abstract use of polymorphism. but still totally practical.
These demonstrate and test the use of
enable_ref_ptr_to_this, and its
ref_ptr_to_this method and the
There is a modeless dialog which demonstrates and tests the use of:
enable_self_delete and its
There is a font pool which demonstrates and tests the use of:
It is a WTL application - you will have to install the WTL library - free from SourceForge.
It does seem to work quite reliably, and most importantly, it solves rather than hides the problem of the programmer forgetting to
null secondary references by nulling them automatically. I have adopted it for everything new that I do, and I have retrospectively implemented it in much of the library code that I have written. None of this has given me any problems, so I feel I can recommend it to others with the argument that it puts C++ ahead of C# and Java in terms of both coding comfort and memory and pointer safety without compromising how C++ works.
I am not an expert on smart pointers. I just needed to have a memory protection system that I could work with that doesn't undermine the integrity of good C++ programming. If someone who is an expert can re-write this in a more elegant or efficient way, or that more comprehensively assures or verifies its integrity, I will be delighted.
- 14th May, 2008: First submitted.
- 11th June, 2008: Source code and article updated.
- Changes to code:
- Abandoned trying to optimise away the pointer held in the reference controller resulting in a more straightforward operation.
owner_ptr given destructive copy constructor and assignment operator, following the example of
std::auto_ptr, but explicit use in application code not recommended.
- Reference controller pool given mildly exponential growth rate.
- Code layout redesigned to make it more readable, transparent, and verifiable.
- Changes to article sections:
- XONOR Pointers
- Building a Complete Integrated Solution
- Collections of XONOR Pointers
- Rules of Use
- What can Still Bite You
- How it Works
- Transparency of Source Code
- Using the Source Code
- 27th June, 2008: Source code updated to resolve build issues and conformance with Visual Studio 2008.
- 4th February, 2009: Source code and article updated.
- Changes to code:
- Assignment of one
owner_ptr to another made illegal - use explicit
.steal_object dot methods.
- Copy constructor remains legal because its implicit use is required by collections.
- Changes to
ref_ptr to support multithreading.
- New article sections:
- Complexities Introduced by Multithreading
- Design Decisions Affecting Multithreading
- How Multithreading Support Works
- Limitations of Multithreading Support