C++ / Intermediate / Lesson 7
RAII, lifetimes, and smart pointers
Tie resource release to object lifetime, distinguish owning handles from borrowers, and select unique or shared ownership only when the design requires it.
On this page
Lesson goals
- Explain deterministic scope cleanup and reverse destruction order.
- Transfer unique_ptr ownership without copying it.
- Recognize shared_ptr cycles and the purpose of weak_ptr.
A resource needs an owner
A resource can be allocated memory, an open file, a held mutex, or another acquired capability. Resource acquisition is initialization, abbreviated RAII, puts ownership in an object whose destructor releases that capability. The name emphasizes construction, but the benefit is reliable cleanup on ordinary scope exit and stack unwinding. A raw pointer normally communicates access without ownership in modern application code. Before selecting a pointer type, ask whether dynamic allocation is necessary at all; a local value or standard container often expresses the simplest lifetime.
Cleanup follows scope structure
Automatic objects are destroyed in reverse order of completed construction when their scope exits. Nested scopes make lifetime boundaries visible. The trace example models acquisition and release with messages rather than allocating a real external resource. B is destroyed before A because its inner scope ends first. A destructor must not let an exception escape during stack unwinding; doing so can terminate the program. For a real file, use an owning file stream and handle explicit flush or close failures where the application needs to observe them.
#include <iostream>
#include <string>
#include <utility>
struct Trace {
std::string name;
explicit Trace(std::string value) : name(std::move(value)) {
std::cout << "acquire " << name << "\n";
}
~Trace() { std::cout << "release " << name << "\n"; }
};
int main() {
Trace outer{"A"};
{ Trace inner{"B"}; std::cout << "work\n"; }
}
acquire A
acquire B
work
release B
release A
The messages show scope order only. Production resource owners should keep destructors non-throwing; output streams can be configured to throw.
Use unique_ptr for one dynamic owner
std::unique_ptr<T> owns one dynamic object by default and deletes it when the owning handle is destroyed. make_unique creates the object and its owner together. Copying the handle is forbidden because it would duplicate exclusive ownership. Moving transfers ownership and leaves the source unique_ptr empty, a specific guarantee stronger than the general moved-from rule. get yields a borrowed pointer; release abandons ownership without destroying the object and is rarely needed. Avoid calling delete on a borrowed pointer or creating a second independent owner from it.
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto first = std::make_unique<int>(42);
auto second = std::move(first);
std::cout << std::boolalpha << (first == nullptr) << " " << *second << "\n";
}
true 42
second owns the same integer. first is guaranteed empty and is never dereferenced; second releases the allocation at scope exit.
Review ownership at every boundary
For each function parameter, distinguish observing a resource, temporarily mutating it, and taking ownership. A T& usually borrows an existing object; a unique_ptr<T> value explicitly receives ownership. Returning a unique_ptr can transfer a dynamically polymorphic result, while returning an ordinary value is simpler for many objects. Document whether a borrower may escape the call. Cleanup cannot be inferred from variable names alone: inspect handle types, deleters, and scope. Test early returns and exception paths as well as successful execution, because those are the paths manual cleanup often misses.
Common errors and fixes
Two owners delete the same object.
- Why it happens
- Independent smart pointers were built from one raw pointer.
- How to fix it
- Transfer an existing unique owner or copy an existing shared owner.
- Prevent it next time
- Create ownership exactly once and keep raw pointers observational.
A shared object is never destroyed.
- Why it happens
- Owning links form a reference-count cycle.
- How to fix it
- Make an appropriate back-link weak and check lock results.
- Prevent it next time
- Draw the ownership graph separately from the access graph.
A moved unique_ptr is dereferenced.
- Why it happens
- The source was assumed to retain ownership.
- How to fix it
- Use the destination owner or initialize a new source value.
- Prevent it next time
- Treat ownership transfer as a visible state transition.
Check your understanding
1. A function must consume a dynamically allocated widget and retain it after returning. Choose between Widget&, Widget*, and std::unique_ptr<Widget> by value.
Show worked solution
Use unique_ptr<Widget> by value to make ownership transfer explicit. The caller passes std::move(owner), and the function moves the parameter into its retained owner. A reference or raw pointer would only borrow unless a separate unusual contract said otherwise. Check the input owner if null is invalid; otherwise document an empty-handle case.
2. Two nodes own each other with shared_ptr. Explain why dropping both external handles is insufficient and how a weak back-link changes destruction.
Show worked solution
Each node still has an incoming owning reference from the other node, so neither count becomes zero. Replace the logically non-owning back-link with weak_ptr. When external ownership of the first node ends, destruction can release its owning link and allow the second node to be destroyed too. Use lock before observing a weak target because it may have expired.
Key takeaways
- Tie cleanup to owning object lifetimes.
- Prefer values, then unique ownership, then justified sharing.
- A borrower must never outlive its resource.