Smart pointers and owning views
Unique ownership is the default
The Core Guidelines (R.20, R.21) require that a heap object have exactly one owning smart pointer. std::unique_ptr<T> satisfies this rule. It holds a pointer, destroys the object when the unique_ptr itself is destroyed, and can be moved but never copied. The move operation transfers the stored pointer and leaves the source empty.
#include <memory>
#include <vector>
#include <print>
struct tree_node {
int value{};
std::vector<std::unique_ptr<tree_node>> children;
};
void print(const tree_node& node) {
std::print("{} ", node.value);
for (const auto& child : node.children) {
print(*child);
}
}
int main() {
auto root = std::make_unique<tree_node>();
root->value = 1;
tree_node* cur = root.get();
for (int v = 2; v <= 5; ++v) {
cur->children.emplace_back(std::make_unique<tree_node>());
cur->children.back()->value = v;
cur = cur->children.back().get();
}
print(*root);
std::println("");
return 0;
}
In the tree example the root is a std::unique_ptr<tree_node>. Each node stores its children in a std::vector<std::unique_ptr<tree_node>>. Because every child is owned uniquely, the destruction of the root automatically destroys the whole subtree recursively. No manual delete appears, and the program cannot accidentally copy a node-owner. The compiler rejects any copy of a unique_ptr.
Transfer of ownership by value
A function that receives a unique_ptr<T> by value takes ownership. The caller must move the pointer into the parameter. After the call the caller’s pointer becomes empty. This pattern appears in many factory functions:
std::unique_ptr<tree_node> make_root(int v) {
auto p = std::make_unique<tree_node>();
p->value = v;
return p; // move-return, caller receives ownership
}
In the tree example the statement auto root = std::make_unique<tree_node>() creates the sole owner. When main ends, root goes out of scope, the move-return chain unwinds, and the destructor of each unique_ptr in the vectors frees the corresponding child. No memory leaks survive past main.
std::make_unique versus new
The guidelines (R.23) require that a unique_ptr be created with std::make_unique rather than a raw new expression. std::make_unique<T>(args...) constructs the object and wraps it in a unique_ptr in one step. This form is shorter and safer than the two-step alternative. The two-step form first calls new T(args...) and then passes the result to the unique_ptr constructor. If an exception occurs between these two steps, the raw pointer leaks. std::make_unique avoids this window, because the construction and the wrapping happen together. The function also deduces the type, so the code does not repeat the type name. Use std::make_unique whenever the object is created and owned immediately. Reserve a raw new for the rare case where a custom deleter or a pre-existing pointer is required.
Move semantics give unique_ptr its efficiency. Moving a unique_ptr transfers the pointer without copying the pointed-to object. The operation is a simple pointer assignment plus a null-out of the source. It never allocates and never touches the heap object. This property lets a function return a unique_ptr by value at no cost. The move-return in make_root does not copy the tree. It hands the same object to the caller. The same reasoning applies when a unique_ptr moves into a container or into another owner.
Shared ownership is a cost-aware choice
std::shared_ptr<T> holds a control block with an atomic reference count. Every copy increments the counter. The last copy decrements to zero and destroys the object. The guidelines (R.22) caution that shared_ptr must be used only when at least two distinct owners truly need to keep the object alive.
The cost model is higher than unique_ptr:
| Aspect | unique_ptr | shared_ptr |
|---|---|---|
| Allocation | one block (object) | two blocks (object + control) |
| Reference count | none | atomic increment/decrement on every copy |
| Size of pointer object | ≤ sizeof(void*) | ≈ 2 × sizeof(void*) |
| Cache behavior | contiguous access | indirect control block |
If the program never needs more than one owner, unique_ptr is the zero‑overhead choice. shared_ptr allocates an extra control block and incurs atomic reference‑count updates on each copy, doubling pointer size and reducing cache locality. Use shared_ptr only when at least two owners truly need to keep the object alive. Otherwise the extra cost is unnecessary. |
Weak pointers break cycles
std::weak_ptr<T> provides a non‑owning view that does not affect the reference count, breaking reference cycles. Use lock() to obtain a temporary shared_ptr if the object is still alive. Otherwise lock() returns an empty pointer. expired() reports whether the object has been destroyed. In the graph example, weak_ptr allows the parent link to be observed without extending the child’s lifetime, preventing a cycle.
#include <memory>
#include <vector>
#include <print>
struct graph_node {
int id{};
std::weak_ptr<graph_node> parent;
std::vector<std::shared_ptr<graph_node>> children;
};
void print_ids(const graph_node& node) {
std::print("{} ", node.id);
for (const auto& child : node.children) {
print_ids(*child);
}
}
int main() {
auto a = std::make_shared<graph_node>();
a->id = 1;
auto b = std::make_shared<graph_node>();
b->id = 2;
// create edge a -> b and back-edge parent weak_ptr
a->children.push_back(b);
b->parent = a; // weak, no cycle
print_ids(*a);
std::println("");
// when main exits, both a and b are reclaimed; no leak.
return 0;
}
gsl::owner<T> documents raw owning pointers
Sometimes a C-language API requires a raw pointer that owns the pointed-to object. The Guidelines Support Library provides gsl::owner<T> as a type alias that makes the ownership intent explicit to static analysis tools:
void c_api(gsl::owner<int*> p); // p must be freed by the caller
gsl::owner<T> is a type alias that documents raw owning pointers for static analysis tools such as Clang’s lifetime‑safety analysis. It carries no runtime cost and does not alter program behavior. A function taking a gsl::owner<T> parameter signals that it assumes ownership. A function returning gsl::owner<T> signals that it transfers ownership to the caller. Static analysis can verify that each owner releases the object exactly once.
When pointers are the wrong tool
Ownership and lifetime are not the only reasons to use a pointer. Frequently a value, a std::span, or a std::string_view conveys the required relationship without any ownership semantics.
- Value: use when the object’s lifetime is confined to the current scope and copying is cheap.
std::span<T>: a non-owning view over a contiguous range. It is ideal for passing array slices to functions.std::string_view: a read-only view of a string. It is perfect for read-only parameters where the callee must not modify or own the data. Choosing a smart pointer when a simple view suffices adds unnecessary indirection and can hide bugs. The guidelines (R.3) advise to prefer plain values and views first. Smart pointers come into play only when the lifetime must outlive the current scope and no value can express the relation. A value owns its data within the current scope. Copying or moving it transfers ownership automatically.std::span<T>provides a non‑owning view of a contiguous range, andstd::string_viewprovides a read‑only view of a string. These types express the relationship without ownership overhead and avoid the need for raw pointers. Use them before considering a smart pointer.
Try this
Take the tree program from the previous section and add a function:
int height(const tree_node&);
height returns the length of the longest root-to-leaf path (the number of nodes on that path). Write the function recursively, using only the tree_node interface. When you run the program, observe that the tree is still freed automatically when main exits, even though height returns a plain int.
What does std::unique_ptr guarantee about the tree’s memory after height returns?