Portfollio

Menu

Tricky C++: Making Memory Management Easier with Smart Pointers

One of the problems with writing programs in C++ is that using dynamic memory can be troublesome as it requires manually managing the allocation and deallocation of the memory you want to use. Unfortunately, dynamic allocation is an important feature in C++ and is required when using virtual functions, for polymorphic behaviour. Dynamic allocation is also required if you intend to store data that is larger than the stack (over 1MB), or of an unknown size at compile time, for instance, data received as input from a user. There are other benefits such as allowing the creation of dynamic data structures, and allowing objects to exist beyond their original scope. Also did I mention you have to use pointers when allocating memory dynamically.

The problems that occurs with dynamic allocation relates to the manual memory management as mentioned previously. Two important issues that can arise are dangling pointers and memory leaks. The former refers to an instance in which a pointer attempts to access memory that has previously been freed. The latter relates to forgetting to deallocate memory leaving it inaccessible and unusable during the execution of the program. If this is done repeatedly, the amount of memory available to other programs reduces, eventually causing instability and crashes of the system. Therefore, there is a significant risk involved in using dynamic memory, however it is sometimes unavoidable. With the release of C++ 11, came the introduction of smart pointers, a feature that makes it easier to handle dynamic memory.

Smart Pointers

​Smart pointers avoid manual memory management by automatically managing the memory for you. They can be a little more confusing to understand than raw pointers, mainly due to their being several different types: shared, weak, and unique pointers. The difference between these pointers is in the type of ownership semantic they implement. Remember that one way of helping to reduce memory management issues is to define who owns the object so that it is the owner, and just the owners responsibility to handle the creation and destruction of the object.

Types

A unique pointer (unique_ptr) is a smart pointer that has exclusive ownership semantics. What this means is that the resource can only be owned by the unique pointer, and when this pointer goes out of scope, the resource is released. The only way for a unique_ptr to “share” the resource is to transfer ownership of that resource using the move semantic.

A shared pointer (shared_ptr) uses the concept of shared ownership semantics. Multiple shared pointers can refer to a single object, when another shared pointer refers to a resource, a reference count is increased. Only when the last shared pointer goes out of scope, is the memory released.

A weak pointer (weak_ptr) acts like a shared pointer in that it provides sharing semantics, however a weak pointer does not own the resource. Meaning that someone else, a shared pointer, must already own a resource that weak pointer can share. Furthermore, the number of weak pointers do not contribute to the reference count of a resource, therefore a resource will be released even if weak pointers still share that resource. This means that a weak pointer cannot access the object it shares. In order to do so a shared pointer must be created from this weak pointer.The reason this is done is so that the resource cannot be destroyed whilst being used by the weak pointer because there will be at least one shared pointer accessing the resource. This does not prevent a weak pointer from accessing a resource that no longer exists.

Usage

This all well and good though but you may be asking when should you use each pointer. Well like most of programming it depends. However understanding the ownership semantics of each type goes a long way.

​A unique pointer is meant solely for single ownership. For example, if you were to have an engine class that had the responsibility of creating and initialising the subsystems of an engine, like the renderer, or physics system. Then it is not unreasonable to assume that the Engine class would be the sole owner of each subsystem and would store each in a unique pointer. When the Engine class is destroyed which would typically be when the game using it is closed, then all the subsystems would be shut down, and the resources related to these systems freed when the instance of Engine goes out of scope. Although ownership remains with Engine, the subsystems can still be accessed by other parts of the game engine by referring to the pointer stored by the unique pointer, accessed through the get() function. This allows other objects access to the resource, the desired system.

A weak pointer doesn’t have ownership but has the ability to share a resource and can be constructed to share the resources of a shared pointer, not a unique pointer. Therefore, a weak pointer can be useful in situations involving shared pointers where you don’t want a function or class accessing the resources of the shared pointer to also control ownership of the resource. Going back to the idea of a game engine again. If we were to have a game object class that stored a list of all the components attached to it, suppose we wanted the components to communicate with other components in the same game object. One idea would be to allow the component to hold a reference to the its parent, the game object. This reference could be stored as weak pointer because it doesn’t make sense for the component to be able to destroy the game object, its parent The game object would most likely have ownership over the components, and these components would be destroyed before the game object.

​A shared pointer can be used in situations in which a unique or weak pointer are implemented except for when using them causes a cyclic dependency. This relates back to the second example with game objects and components. If we have a shared pointer in game object that stores a component, and a component stored in the game object holds a shared pointer to the game object, its parent as shown below.

In the example shown above neither the spaceship or laserWeapon objects will be deleted. When both objects are created a reference count of 1 is set for both. When gameObject holds a reference to laserWeapon the reference count is incremented to 2, and the same occurs when laserWeapon holds a reference to spaceShip. When the program reaches the end of main both objects will go out of scope, and thus the reference count for both is reduced to 1 because we have lost one owner. laserWeapon’s reference count won’t hit 0 until spaceShip’s reference count hits 0, and vice versa, creating a circular dependency that results in the memory of both never getting deleted. Neither reference count hits 0, so C++ doesn’t think that the memory has to be deallocated.