Course outline

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.

About 4 min readContent reviewed
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.

cpp Visible nested lifetimes
#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"; }
}
Expected output
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.

cpp Ownership moves once
#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";
}
Expected output
true 42

second owns the same integer. first is guaranteed empty and is never dereferenced; second releases the allocation at scope exit.

Shared ownership has a real cost

std::shared_ptr models a resource with multiple owners using a control block. The managed object is destroyed when the final owning handle is gone. A weak_ptr observes that control block without keeping the object alive; lock attempts to acquire a live shared owner. Cycles made entirely of shared owners prevent the ownership count from reaching zero, so represent back-links with weak_ptr where appropriate. Thread-safe updates of a shared control block do not make the pointed-to object thread-safe. Prefer unique ownership unless independent lifetime sharing is part of the requirement.

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.

Official references