Concepts / Smart Pointers: unique_ptr and shared_ptr

Smart Pointers: unique_ptr and shared_ptr

Smart pointers are C++ classes that manage dynamically allocated objects automatically.

  • Programming

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

Definition

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.

cpp
Notes on the code
  • 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.

Constructor runsfopen acquires the fileBlock code uses filecaller never closes itScope endsany exit pathDestructor runsfclose releases the filethenthentriggers
Over the course of a block that uses a ScopedFile, when is the resource acquired and when is it released relative to the constructor, the code in the block, and the destructor?

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.

  1. 1
    Step 1: The caller creates a ScopedFile object inside a block, passing a path and a mode.
  2. 2
    Step 2: The constructor calls the file-opening function, so the resource comes into existence while the object is being set up.
  3. 3
    Step 3: The caller works with the file through get(), and the block runs its normal code.
  4. 4
    Step 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.
  5. 5
    Step 5: The destructor runs and closes the file. The resource is gone, and no line in the caller's code asked for it.
Worked example

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?

  1. 1
    Acquire

    The ScopedFile constructor runs when the local object is created, and fopen opens log.txt.

  2. 2
    Use

    The function uses get() to write to the open handle.

  3. 3
    Leave scope

    The function returns, so the local ScopedFile object leaves its scope.

  4. 4
    Release

    The destructor runs and calls fclose. The programmer never wrote a close call.

Result

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.

Enter scopeOwner objectconstructedresource acquiredCode in scope runsException thrown?Normal end of scopeOwner destructor runsresource releasedControl leaves scopeScope left byexceptionnoyes
When an exception is thrown partway through a scope, which steps run, and at which point is the resource released?

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.

cpp
Notes on the code
  • make_unique creates 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.
pointer aholds the objectpointer aemptypointer bemptypointer bholds the object
After std::move hands a unique_ptr to another scope or variable, which object holds the resource, and what does the original pointer hold now?

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.

cpp
Notes on the code
  • Copying a shared_ptr adds 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.
One ownercount 1Two ownerscount 2One owner remainingcount 1Object releasedcount 0copy createdcopy leaves scopelast owner leaves scope
As shared_ptr copies are created and go out of scope, how does the count of owners change, and at what count is the managed object finally released?

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.

Questionunique_ptrshared_ptr
How many owners at once?Exactly oneSeveral can refer to the same object
How ownership is passed onMoved with std::move; the original is left emptyCopied, which adds another owner
When the object is releasedWhen the single owner is destroyedWhen the last owner is destroyed
Typical design signalOne part of the program is responsible for the objectSeveral 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 like

    A function opens a file, does its work, and then calls fclose at the bottom.

    Why it fails

    An early return or an exception skips the bottom of the function, so the release never runs.

    Do this instead

    Move 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 like
    Report* raw = new Report(); std::unique_ptr<Report> p1(raw); std::unique_ptr<Report> p2(raw);
    Why it fails

    Both 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 instead

    Give 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 like

    auto b = std::move(a); then reading a-> as if it still held the object.

    Why it fails

    Moving leaves the original unique_ptr empty, so it no longer owns anything to use.

    Do this instead

    Use 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 like

    Expecting the object to be destroyed as soon as the inner block ends, even though another copy still exists outside it.

    Why it fails

    The object is released only when the last owner is gone, so other owners keep it alive.

    Do this instead

    Count the owners. The release happens when the count reaches zero.

Trace a Release Point

MEDIUM

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.
EASY

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_ptr expresses a single owner, and std::move transfers that ownership, leaving the original empty without duplicating the resource.
  • A shared_ptr lets 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.

Related concepts

More concepts starting with S