Deciding what smart pointer to use is a question of ownership. When it comes to resource management, object A owns object B if it is in control of the lifetime of object B. For example, member variables are owned by their respective objects because the lifetime of member variables is tied to the lifetime of the object. You choose smart pointers based on how the object is owned.
Note that ownership in a software system is separate from ownership as we would think of it outside of software. For example, a person might "own" their home, but that doesn't necessarily mean that a Person
object has control over the lifetime of a House
object. Conflating these real world concepts with software concepts is a sure-fire way to program yourself into a hole.
If you have sole ownership of the object, use std::unique_ptr<T>
.
If you have shared ownership of the object...
- If there are no cycles in ownership, use std::shared_ptr<T>
.
- If there are cycles, define a "direction" and use std::shared_ptr<T>
in one direction and std::weak_ptr<T>
in the other.
If the object owns you, but there is potential of having no owner, use normal pointers T*
(e.g. parent pointers).
If the object owns you (or otherwise has guaranteed existence), use references T&
.
Caveat: Be aware of the costs of smart pointers. In memory or performance limited environments, it could be beneficial to just use normal pointers with a more manual scheme for managing memory.
The costs:
- If you have a custom deleter (e.g. you use allocation pools) then this will incur overhead per pointer that may be easily avoided by manual deletion.
std::shared_ptr
has the overhead of a reference count increment on copy, plus a decrement on destruction followed by a 0-count check with deletion of the held object. Depending on the implementation, this can bloat your code and cause performance issues.
- Compile time. As with all templates, smart pointers contribute negatively to compile times.
Examples:
struct BinaryTree
{
Tree* m_parent;
std::unique_ptr<BinaryTree> m_children[2]; // or use std::array...
};
A binary tree does not own its parent, but the existence of a tree implies the existence of its parent (or nullptr
for root), so that uses a normal pointer. A binary tree (with value semantics) has sole ownership of its children, so those are std::unique_ptr
.
struct ListNode
{
std::shared_ptr<ListNode> m_next;
std::weak_ptr<ListNode> m_prev;
};
Here, the list node owns its next and previous lists, so we define a direction and use shared_ptr
for next and weak_ptr
for prev to break the cycle.