Smart Pointer Tutorial
Smart pointers are a very helpful tool to avoid manual memory management and all the issues connected with it (memory leaks, unclear ownership semantics, double deletions, exception safety, ...). Basically, smart pointers are class templates that encapsulate a raw pointer and free memory in the destructor. This makes RAII possible, a fundamental idiom for resource management in modern C++.
C++ libraries contain various smart pointer implementations which mainly differ in the way ownership is managed. The most important ownership semantics are:
- Unique ownership
There is only one smart pointer referring to an object at the same time and the smart pointer cannot be copied.
Examples:boost::scoped_ptr
,std::unique_ptr
(C++11, can be explicitly moved) - Shared ownership
Multiple smart pointers can refer to a single object, a reference counter is responsible that the last smart pointer deallocates memory.
Example:std::shared_ptr
(C++11) - Copied ownership
Every time a smart pointer is copied, the object behind it is also copied. This is extremely useful in combination with polymorphism.
Example:aurora::CopiedPtr
Design decisions
For many use cases, the well-known smart pointers (most notably shared_ptr
and unique_ptr
) are not enough. Aurora provides the class template aurora::CopiedPtr
which implements a smart pointer with copied ownership. It has been conceived with the following points in mind:
- Well-known conventions
Dereferencing operators * and -> and a conversion to a bool-like type imitate raw pointer syntax. Special member functions such asreset()
orswap()
are named like the standard equivalents. Pointee-levelconst
qualifiers can be added, but not removed. - No implicit conversions
Conversions from and to raw pointer types without casts or constructor calls are forbidden. While these conversions may seem convenient, they can lead to unwanted behavior in many situations and don't express the presence of a smart pointer in the code. - RAII and value semantics
Aurora's SmartPtr module encourages the use of the RAII idiom to relieve yourself from manual memory management. This is not only less error-prone, but makes code much simpler, especially in presence of multiple return paths and exceptions. Operations like copy, assign, pass to/from function have the same semantics as value types like int. - Cloner and deleter strategies using type erasure
Lets the user specify cloners and deleters at runtime. The type itself (namelyCopiedPtr<T>
) is not affected, therefore cloners and deleters are abstracted from the user after initialization. This approach makes it possible to pass lambda expressions as cloner or deleter functions.
Basic operations
The operators *
and ->
for dereferencing as well as a conversion to a bool-like type to test for validity are provided.
aurora::CopiedPtr<MyClass> ptr; if (ptr) // ptr points to a valid object if (!ptr) // ptr is empty (nullptr)
The member function reset()
sets the pointer to a new object or to nullptr, when no argument is passed.
ptr.reset(new MyClass); // Destroys old object, assigns new object ptr.reset(); // Destroys old object, assigns nullptr
Deep copies
The C++11 standard library and Boost don't provide any smart pointers with deep copy semantics at all. That is, you cannot copy the referenced object in general. The approach new T(*ptr)
doesn't work because T might be polymorphic and you don't know the dynamic type, or there might even be no copy constructor.
aurora::CopiedPtr
is able to perform deep copies. That means, the pointee (object pointed to) is copied when the pointer is copied.
aurora::CopiedPtr<MyClass> first(new MyClass); aurora::CopiedPtr<MyClass> second = first; // Object in first is copied to second, *first == *second
Things become more interesting when dynamic polymorphism is involved. Let's assume we have the following class hierarchy (we abandon const-correctness for simplicity):
class B { public: virtual ~B() {} virtual B* clone() = 0; virtual int number() = 0; }; class D : public B { public: virtual D* clone() { return new D(*this); } virtual int number() { return 1; } }; class E : public B { public: virtual E* clone() { return new E(*this); } virtual int number() { return 2; } };
The virtual function clone()
returns an identical object of the derived type. Now we declare two base smart pointers with the aurora::VirtualClone
cloner to make use of the clone()
function for deep copying.
aurora::CopiedPtr<B> p(new D, aurora::VirtualClone<D>()); // p->number() == 1 aurora::CopiedPtr<B> q(new E, aurora::VirtualClone<E>()); // q->number() == 2 q = p; // Perform deep copy, destroy E object. Invokes q.clone() internally. // p->number() == 1 and q->number() == 1
The smart pointer objects have usual value semantics: We can copy and assign them, and internally the right derived object is chosen. The deep copy is safe, no object slicing occurs.
But defining this clone()
function in every class in inconvenient. Forgetting it may lead to object slicing if the function has already been defined in a base class. Unfortunately, we have to define it, or how should the pointer know in which way the object is copied?
What if I told you that we actually don't? Indeed, no clone()
function is required to perform deep copies across a polymorphic class hierarchy. And don't be afraid: We don't need any dynamic_cast
, typeid
, switch
or if-else
-cascade either. There is a very elegant solution encapsulated in aurora::CopiedPtr
combined with the aurora::OperatorNewCopy
policy.
After removing all the clone()
methods from the three classes, we can write the following, just as above:
aurora::CopiedPtr<B> p, q; p.reset(new D); // p->number() == 1 q.reset(new E); // q->number() == 2 q = p; // Perform deep copy, destroy E object // p->number() == 1 and q->number() == 1
It does work. In case you wonder why: The constructor and reset()
are function templates that recognize the actual type of the new
-expression. By knowing the dynamic type, it is possible to invoke the corresponding copy constructor. On the other side, you can't do something like this:
aurora::CopiedPtr<B> p, q;
B* raw = new D;
p.reset(raw);
q = p;
The pointer passed to reset()
is of type B*
and not D*
. Here, the aurora::VirtualClone
policy would work again.
Advantages
CopiedPtr<T> vs. T
You might ask yourself why one wouldn't directly use T
. There are several advantages of CopiedPtr<T>
over T
:
- It works with polymorphic types
- It works with noncopyable types (if there's no copy constructor, you can specify a different cloner strategy)
- The type
T
needn't be complete at the declaration ofCopiedPtr<T>
, which reduces header dependencies - The smart pointer is movable even if
T
itself is not
CopiedPtr<T> vs. T*
One can still use raw pointers to achieve some of the points mentioned above. The reasons to prefer CopiedPtr<T>
are:
- RAII, no memory management, exception safety
- Once the pointer has been initialized, it automatically knows how to copy and destroy its objects. This knowledge is hidden from the user, so one can limit interfaces to a single
CopiedPtr<T>
type that works with arbitrary cloners and deleters.
Limitations
Of course, aurora::CopiedPtr
is not the ultimate solution to every problem. There are limitations which are useful to know:
- Because cloners and deleters can be assigned at runtime, they bring some performance overhead. This concerns memory usage as well as runtime speed during creation, copy construction and destruction of smart pointers (but not dereferencing). So if you don't exploit these features, you'd better use a more basic smart pointer like
std::unique_ptr
. - The
aurora::OperatorNewCopy
strategy (default cloner) requires a pointer to the derived type to be passed at initialization. You cannot initializeCopiedPtr<Base>
withBase*
unless you use a different cloner, likeaurora::VirtualClone
.