Smart Pointers: unique_ptr and shared_ptr
Smart pointers are C++ classes that manage dynamically allocated objects automatically.
Why Manual Cleanup Breaks
Many programs take hold of something that must later be given back. A program may hold memory, an open file, a running thread, or a lock that other parts of the program need. If the release step is written by hand, it is easy to skip it, especially when a function returns early or an error interrupts the normal flow. The result is a leak, or a resource that stays locked or open far longer than it should.
C++ offers a different approach. Rather than trusting the programmer to remember every release, the language lets the lifetime of an ordinary object decide when cleanup happens. The idiom that does this is called RAII, which stands for Resource Acquisition Is Initialization. The name is long, but the idea is compact: obtaining a resource happens while an object is being set up, and giving the resource back happens while that object is being destroyed. This article follows that idea from the first acquisition to the points where ownership changes hands.
Tying Release to Object Lifetime
RAII (Resource Acquisition Is Initialization) is the idiom that ties acquiring a resource to the construction of an object and releasing it to the destruction of that object, so the owner's scope decides when cleanup happens.
Consider a small class that owns a C standard library file handle. The constructor performs the acquisition by opening the file. The destructor performs the release by closing it. Because the destructor runs automatically when the object leaves its scope, the programmer does not write a close call at each exit point. The class below is written for this article to illustrate the rule described in the source.
- The constructor acquires the file handle while the object is being set up.
- The destructor releases the file handle while the object is being destroyed.
- No caller ever writes a close call; the release is written once, in the destructor.
The key point is where the release is written. It is written once, in the destructor, and not at every place a function might leave. Each resource has one owner, and by default that owner releases the resource when its scope ends. In the class above, the owner is the ScopedFile object, and its scope determines when the file closes.
Tracing a ScopedFile Through Its Scope
The sequence below traces what happens when a caller uses the ScopedFile class. Read it from top to bottom. The construction step is where the resource comes into existence, and the destruction step is where it goes away. Notice that the caller never issues a release request of its own.
- 1Step 1: The caller creates a ScopedFile object inside a block, passing a path and a mode.
- 2Step 2: The constructor calls the file-opening function, so the resource comes into existence while the object is being set up.
- 3Step 3: The caller works with the file through
get(), and the block runs its normal code. - 4Step 4: The caller does not decide to close the file. The scope decides. Whatever path the caller takes out of the block, the object is destroyed.
- 5Step 5: The destructor runs and closes the file. The resource is gone, and no line in the caller's code asked for it.
Following one ScopedFile from start to finish
A function creates a ScopedFile for log.txt, writes to it, and then returns. Where does the file get closed?
- 1Acquire
The ScopedFile constructor runs when the local object is created, and fopen opens
log.txt. - 2Use
The function uses
get()to write to the open handle. - 3Leave scope
The function returns, so the local ScopedFile object leaves its scope.
- 4Release
The destructor runs and calls fclose. The programmer never wrote a close call.
The file is closed at the moment the owning object is destroyed, which is when the function's scope ends.
When an Exception Interrupts the Scope
RAII is integrated with error handling in the form of exceptions. This matters because an exception can leave a scope before the code at the bottom of a function runs. A hand-written release placed at the end of a function would be skipped in that case. An object owned by a scope is still destroyed as the scope is left, so its release still happens.
Both paths in the flowchart meet at the same destructor. Whether the scope ends normally or an exception carries control out of it, the owning object is destroyed as the scope is left, so the release is not skipped.
Ownership With unique_ptr
Ownership is not always fixed to the scope where a resource was first obtained. Resources can be moved from one scope to another, either through move semantics or through smart pointers. When ownership moves, the resource is not duplicated. Instead, one owner gives up its claim and another owner takes it on, so the release still happens exactly once.
Smart pointers are C++ classes that manage dynamically allocated objects automatically. A std::unique_ptr expresses a single owner: only one unique_ptr holds the object at a time, and when that unique_ptr is destroyed, the object is destroyed with it. Ownership can be transferred to another unique_ptr with std::move, which leaves the original empty.
make_uniquecreates the single owner.- std::move transfers ownership to b and leaves a empty.
- Passing std::
move(b)to consume moves the duty of release into that function's scope.
The before and after states show the central rule. Exactly one owner exists at any moment, and the resource is released by whichever owner holds it when its scope ends. Moving the pointer changes which scope holds that duty, and nothing else. The object itself is not copied and is not released during the transfer.
Shared Ownership With shared_ptr
Sometimes one object cannot be assigned to a single owner. A std::shared_ptr lets several owners refer to the same object. Each owner has a say in the object's lifetime, so the object is not released just because one of the owners leaves its scope. The object is released only after the last owner is gone.
- Copying a
shared_ptradds an owner rather than moving the duty away. - The inner block ends, but the object survives because first still owns it.
- The object is released only when the last remaining owner is destroyed.
The count is what makes shared ownership work. Each copy adds one to the number of owners, and each owner that leaves its scope takes one away. The object is released only when that number drops to zero, which is why the release still follows the scope of the owners rather than the scope of any single one of them.
Choosing Between the Two Owners
Choosing between the two smart pointers comes down to the question of ownership. If the design has exactly one owner, unique_ptr states that clearly. If several parts of the program must keep the object alive, shared_ptr represents that situation. In both cases the release is still tied to scope, which is why the same RAII reasoning applies.
| Question | unique_ptr | shared_ptr |
|---|---|---|
| How many owners at once? | Exactly one | Several can refer to the same object |
| How ownership is passed on | Moved with std::move; the original is left empty | Copied, which adds another owner |
| When the object is released | When the single owner is destroyed | When the last owner is destroyed |
| Typical design signal | One part of the program is responsible for the object | Several parts must keep the object alive |
Choosing between unique_ptr and shared_ptr by ownership question
Mistakes That Break RAII Cleanup
RAII only works while each resource keeps a single, clear owner whose scope ends in the right place. The mistakes below are the usual ways that guarantee is lost.
Common mistake Writing the release by hand at the end of a function
What it looks likeA function opens a file, does its work, and then calls fclose at the bottom.
Why it failsAn early return or an exception skips the bottom of the function, so the release never runs.
Do this insteadMove the release into a destructor of an owning object so the scope ends the resource's life on every exit path.
Common mistake Creating two owners from one raw pointer
What it looks likeReport* raw = new Report(); std::unique_ptr<Report> p1(raw); std::unique_ptr<Report> p2(raw);Why it failsBoth smart pointers believe they are the sole owner of the same object. When each one is destroyed it tries to release that object, which produces a double free.
Do this insteadGive the raw pointer to exactly one owner, or create the object with std::make_unique so that no second owner can be made from the same raw pointer.
Common mistake Using a unique_ptr after moving from it
What it looks likeauto b = std::move(a); then reading a-> as if it still held the object.
Why it failsMoving leaves the original
unique_ptrempty, so it no longer owns anything to use.Do this insteadUse the new owner, b, after the move, and check the pointer before using it if it may be empty.
Common mistake Assuming shared_ptr releases when any one copy leaves scope
What it looks likeExpecting the object to be destroyed as soon as the inner block ends, even though another copy still exists outside it.
Why it failsThe object is released only when the last owner is gone, so other owners keep it alive.
Do this insteadCount the owners. The release happens when the count reaches zero.
Trace a Release Point
A function declares a ScopedFile named f for data.bin, then calls a helper that throws an exception before the function's last line. Trace where the file is closed. Which object's destructor is responsible, and does the release depend on the function reaching its final line?
Hints
- Ask which object owns the file handle and what ends that object's life.
- Compare the throw path with the normal return path in the flowchart.
- The release belongs to the destructor, not to any line in the function body.
Two functions share one std::shared_ptr to the same object. The first function returns while the second still holds a copy. Does the object get released when the first function returns, and what must happen before it is released?
Hints
- Count the owners before and after the first function returns.
- The object is released when the owner count reaches zero.
Key Takeaways
- RAII ties acquiring a resource to an object's construction and releasing it to that object's destruction, so the owner's scope decides when cleanup happens.
- Because the destructor runs as a scope is left, the release still happens when an exception interrupts normal flow and the code at the bottom of a function is skipped.
- A
unique_ptrexpresses a single owner, and std::move transfers that ownership, leaving the original empty without duplicating the resource. - A
shared_ptrlets several owners refer to one object, and the object is released only when the last owner is gone. - The most common ways to break RAII are releasing by hand, creating two owners from one raw pointer, and using a pointer after it has been moved from.