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
,thor::ScopedPtr, std::unique_ptr
(C++11, can be explicitly moved however)Moved ownership
The object is still owned by only one pointer, but the ownership can be implicitly transferred to other smart pointers.
Examples:std::auto_ptr
,thor::MovedPtr
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::tr1::shared_ptr
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:thor::CopiedPtr
Design
With the C++98 standard library and TR1, there are three smart pointers available: auto_ptr
, shared_ptr
and weak_ptr
(which is always used together with shared_ptr
). For many use cases, this is not enough. Thor provides three additional smart pointer types that are ready-to-use for arbitrary types. The intention is to abstract from low-level techniques like manual memory management, overloads of The Big Three, and worries about pointer validity.
Thor's SmartPtr module comes with low requirements. It is header-only and completely independent of SFML, so you can directly use it in your project.
For every smart pointer in Thor, the pointee type needn't be complete at the time of declaration. This behavior is adapted from raw pointers.
class Pointee; // incomplete type Pointee* rawPointer; thor::ScopedPtr<Pointee> smartPointer;
The type needs only be complete when you initialize the smart pointer with a valid pointer. The advantage of this technique are lower dependencies, especially in header files. When you have the following class definition, there is no need to include the definition of the type Pointee
, so the client doesn't have to know it. As a result, the compilation is faster, and changes in the definition of Pointee
require no recompilation. This helps you to implement idioms such as Pimpl.
class Pointee; // forward declaration class MyClass { thor::ScopedPtr<Pointee> ptr; };
Furthermore, Thor's smart pointers do not provide implicit conversions from and to raw pointer types. While these conversions may seem convenient, they can lead to unwanted behavior in many situations.
Basic operations
All smart pointers support the overloaded operators *
and ->
for dereferencing as well as a conversion to a bool-like type to test for validity.
thor::ScopedPtr<MyClass> ptr; if (ptr) // ptr points to a valid object if (!ptr) // ptr is empty (NULL)
The member function Reset()
sets the pointer to a new object or to NULL, when no argument is passed.
ptr.Reset(new MyClass); // Destroys old object, assigns new object ptr.Reset(); // Destroys old object, assigns NULL
Scoped pointers
The class template thor::ScopedPtr
is a simple smart pointer that invokes the destructor and delete
operator of an object as soon as it goes out of scope. This smart pointer cannot be copied or assigned.
You can use it for the RAII idiom:
int main() { thor::ScopedPtr<MyClass> ptr(new MyClass); ptr->DoSomething(); } // End of block: delete operator is called
Movable pointers
thor::MovedPtr
can be used to transfer ownership from a smart pointer to another. A use case is a factory function that creates an object which is passed to the caller:
thor::MovedPtr<MyClass> CreateObject() { thor::MovedPtr<MyClass> ptr(new MyClass); ptr->DoSomething(); return ptr; }
Smart pointers with implicit move semantics are useful in many situations, but you have to be careful because they don't behave as expected when it comes to copies and assignments. The copy constructor and copy assignment operator of thor::MovedPtr
modify the origin:
thor::MovedPtr<MyClass> first(new MyClass); thor::MovedPtr<MyClass> second = first; // first is now empty, object moved to second
Copyable pointers
This chapter treats smart pointers with deep copy semantics. thor::CopiedPtr
provides three different policies that specify how a copy is performed. You can also use your own policy to customize the behavior. The copy policy corresponds to the second template parameter of thor::CopiedPtr
.
The simplest policy is thor::StaticCopy
, it invokes the copy constructor of the pointee object:
thor::CopiedPtr<MyClass, thor::StaticCopy> first(new MyClass); thor::CopiedPtr<MyClass, thor::StaticCopy> 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 Base { public: virtual ~Base() {} virtual Base* Clone() = 0; virtual int Number() = 0; }; class Derived1 : public Base { public: virtual Derived1* Clone() { return new Derived1(*this); } virtual int Number() { return 1; } }; class Derived2 : public Base { public: virtual Derived2* Clone() { return new Derived2(*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 thor::VirtualClone
policy to make use of the Clone()
function for deep copying.
thor::CopiedPtr<Base, thor::VirtualClone> p, q; p.Reset(new Derived1); // p->Number() == 1 q.Reset(new Derived2); // q->Number() == 2 q = p; // Perform deep copy, destroy Derived2 object // 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 thor::CopiedPtr
combined with the thor::DynamicCopy
policy.
After removing all the Clone()
methods from the three classes, we can write the following, just as above:
thor::CopiedPtr<Base, thor::DynamicCopy> p, q; p.Reset(new Derived1); // p->Number() == 1 q.Reset(new Derived2); // q->Number() == 2 q = p; // Perform deep copy, destroy Derived2 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:
thor::CopiedPtr<Base, thor::DynamicCopy> p, q;
Base* raw = new Derived1;
p.Reset(raw);
q = p;
The pointer passed to Reset()
is of type Base*
and not Derived1*
. Here, the thor::VirtualClone
policy would work again.
Interaction between CopiedPtr and MovedPtr [advanced]
Sometimes we need to take ownership of a CopiedPtr
without copying its content. Basically, the Release()
function can be invoked to take ownership of a pointer:
// Transfer ownership from p to q thor::CopiedPtr<MyClass, thor::StaticCopy> p(new MyClass); thor::CopiedPtr<MyClass, thor::StaticCopy> q(p.Release());
But you need to make sure that the released pointer is immediately assigned to another smart pointer, or you have a memory leak. Anyway, this way becomes problematic as soon as you use the DynamicCopy
policy with polymorphic objects, because Release()
only returns a pointer to the base class. So you lose type information, which is essential for DynamicCopy
in order to function correctly.
There is a much simpler and safer way, namely the function thor::Move()
:
thor::CopiedPtr<Base, thor::DynamicCopy> p(new Derived1); thor::CopiedPtr<Base, thor::DynamicCopy> q = thor::Move(p); // p is now empty, q contains the Derived1 object
The Move()
function returns a MovedPtr
with the same template arguments as the original CopiedPtr
. Type information is retained. You can implicitly convert a MovedPtr
object to a CopiedPtr
object:
thor::MovedPtr<Base, thor::DynamicCopy> p(new Derived1); thor::CopiedPtr<Base, thor::DynamicCopy> q = p; // p is now empty
On the other side, you may want to copy the contents of a MovedPtr
to another MovedPtr
without transferring ownership, here you can use thor::Copy()
:
thor::MovedPtr<Base, thor::DynamicCopy> p(new Derived1); thor::MovedPtr<Base, thor::DynamicCopy> q = thor::Copy(p); thor::CopiedPtr<Base, thor::DynamicCopy> r = thor::Copy(p); // p still contains the original object, q and r hold copies
To sum up, the following conversions are possible:
Conversion | Transfer object | Copy object |
---|---|---|
thor::MovedPtr →thor::MovedPtr |
implicitly | thor::Copy() |
thor::MovedPtr → thor::CopiedPtr |
implicitly | thor::Copy() |
thor::CopiedPtr →thor::MovedPtr |
thor::Move() |
thor::Copy() |
thor::CopiedPtr →thor::CopiedPtr |
thor::Move() |
implictly |