Concepts / Destructor-Based Resource Management

Destructor-Based Resource Management

The resource has to be given back at some point, and the class is the natural place for that to happen.

  • Programming

A Handle That Seems Too Good to Be True

Smart pointers exist for one main reason: to tie the lifetime of a resource to the lifetime of an object, so that cleanup happens automatically instead of depending on someone remembering to release the resource. Most of the time a single owner is enough. One object holds the resource and releases it when it goes away. Sometimes, though, several parts of a program genuinely need to use the same resource, and no single one of them is clearly in charge. shared_ptr is the smart pointer built for that situation.

A common first impression is that a shared_ptr is just a handle. You can copy it into any function that needs the object, the object keeps working, and everything gets cleaned up correctly somewhere in the background. On this view a shared_ptr costs nothing extra, and you can always tell in advance how long the object will live. The automatic cleanup is real. The assumption that goes wrong is that the handle is free and the lifetime is obvious. Try to predict the outcome before reading on.

What do you think happens?

A shared_ptr to one object is copied into three different functions, and each copy is destroyed when its function returns. When is the shared object destroyed?

  • As soon as the first copy is destroyed

  • When the original shared_ptr is destroyed, regardless of the copies

  • When the last shared_ptr that owns it is destroyed

Reveal answer
Answer:

When the last shared_ptr that owns it is destroyed, not when the first one goes away.

Every copy of a shared_ptr is a new owner, and every owner keeps the object alive. The object is destroyed only after the last of those owners is gone, so the destruction point depends on every place that holds a copy.

Why Single Ownership Is the Default

When one object is clearly responsible for a resource, that object holds it and releases it when it is destroyed. The lifetime is visible in one place: the object's scope. This is the simplest design, and it should be your starting point. Shared ownership is a tool for a specific need, not a general way to avoid deciding who owns what.

The contrast with shared_ptr is about how the handle travels. A unique_ptr is moved from place to place, and only one holder exists at a time. A shared_ptr is copied instead. Each copy is a new owner, and each owner keeps the object alive. So when you pass a shared_ptr along, you are not handing over a reference to something someone else controls. You are adding another stake in the object's survival.

Shared Ownership as a Group of Owners

Think of shared ownership as a group of owners attached to a single object. Each shared_ptr is one member of the group. Making a copy adds a member, and destroying a copy removes a member. The object stays alive as long as at least one member remains, and it is destroyed when the group becomes empty. The lifetime is therefore set by the longest-living owner, not by the owner that was created first or the one whose function returned first.

0 owners1 owner: a2 owners: a, b1 owner: a0 owners: objectdestroyedmake_sharedcopy into bb destroyeda destroyed
Each copy raises the number of owners and each destroyed copy lowers it. The object is destroyed at the step where the count reaches zero, not at the step where the first owner leaves.

Notice that the count is the only thing that decides destruction. Where a copy was made, which function holds it, and whether that function has already returned do not matter by themselves. What matters is whether any owner still exists.

Tracing the Last Owner Out of Scope

Follow a concrete case step by step. Owner a is created first. Inside an inner block, owner b is made as a copy of a. When the inner block ends, b is destroyed, but a still exists, so the object survives. When the outer scope ends, a is destroyed, the group is empty, and the object is destroyed at that moment.

cpp
Notes on the code
  • make_shared creates the object and the first owner together
  • The copy makes b a second member of the owner group
  • Leaving the inner block removes b but not the object
a createdb copied from ainner block ends: bdestroyedobject still alive,owned by aouter block ends: adestroyedobject destroyedthenthenthenthenthen
Owner b leaves first, while a still exists, so the object is not destroyed yet. The object is destroyed only when a leaves.

Lifetimes Hidden Inside Tasks

The count-based rule becomes harder to reason about when a copy escapes the code that created it. A function may start a task that holds a copy of a shared_ptr, and that task can outlive the function that created it. In that case a file stays open even after the original caller has finished. The lifetime is still set by the longest-living owner, but that owner may be hidden inside a task you are not watching closely.

Deciding Between Shared and Single Ownership

The practical rule is short: use shared_ptr only if you actually need shared ownership. A shared_ptr is neither free nor very expensive, but it makes the lifetime of the shared object harder to predict. When the need is real, it gives automatic cleanup that respects destructor-based resource management. When the need is not real, a single owner is simpler, easier to reason about, and makes the object's lifetime visible in one place.

QuestionSingle ownerShared ownership with shared_ptr
Who decides when the resource is released?The one owner, when it is destroyedThe last owner to be destroyed
Where is the lifetime visible?In one place, the owner's scopeAcross every place that holds a copy
How is it passed along?Moved, with one holder at a timeCopied, with each copy a new owner
When is it the better fit?One part of the program is clearly in chargeSeveral parts genuinely need the same resource and none is clearly in charge

Single ownership versus shared ownership

Creating Shared Objects with make_shared

Creating an object on the free store and then handing a pointer to it to a smart pointer is logically a little awkward, and it tends to be verbose. The standard library provides make_shared(), declared in the memory header, to create the object and the shared_ptr that owns it in one step. The programmer does not have to write the two-part pattern by hand each time.

Before
std::shared_ptr<Resource> p(new Resource());

The object is created with a separate new expression and then handed to the shared_ptr.

After
std::shared_ptr<Resource> p = std::make_shared<Resource>();

The object and its shared_ptr are created together in one call.

Constructors That Take On a Duty

A class often needs something from outside itself to do its job: memory, a file, a lock, or some other limited resource. When a constructor obtains such a resource, it takes on a responsibility. The resource has to be given back at some point, and the class is the natural place for that to happen. Resource management in C++ is largely about meeting that responsibility reliably, including on paths through the code that a programmer might not expect.

Think of the resource as having two boundaries. The constructor marks the start: this is where the object takes control of the resource. The destructor marks the end: this is where the object gives it back. Between those two points the object can use the resource freely. The guideline is direct: if a constructor acquires a resource, the class needs a destructor to release it. Without that destructor, nothing in the class's own code returns the resource, and it stays held after the object that needed it is gone.

cpp
Notes on the code
  • The constructor acquires heap memory, so it creates the duty
  • The destructor performs the release, attached to the object's end rather than to a line that might be skipped

RAII and Resource Handles

RAII stands for Resource Acquisition Is Initialization. The name describes the pattern: a resource is acquired during initialization of an object, and because of that, the object's lifetime controls the resource. The object's destructor performs the release, so the cleanup is tied to the end of the object's scope.

Definition

A resource handle is an object whose job is to hold a resource on behalf of the code that uses it. The handle acquires the resource when it is created and releases it when it is destroyed, so the programmer's attention can stay on the work rather than on bookkeeping.

Data flows in two directions through this arrangement. Requests and values travel from the calling code into the handle, and the handle forwards them to the resource. Results travel back the same way. The calling code never needs to know when or how the resource is returned, because the handle owns that decision.

Mistakes Beginners Make with Manual Release

Naked new and delete operations put the burden of matching every release on the programmer. A naked new or delete is one that appears directly in ordinary code, with nothing managing the resource it produces. Every naked new must be matched by a delete somewhere else, and the programmer must make sure that match happens on every path through the code. That burden is easy to underestimate, which is why the guideline is to avoid naked new and delete.

cpp
Notes on the code
  • The naked version relies on a delete that the programmer must remember on every path
  • An early return skips the release and leaks the memory
cpp
Notes on the code
  • The managed version attaches the release to the object's scope
  • Every path out of the function releases the array without a manual delete
new int arrayunique_ptr holdsarrayearly returnearly returndelete skipped: leakhandle destroyed:released
In the naked version, a path that returns early can skip the delete. In the managed version, the release is attached to the handle's destruction, so every path reaches it.
  • Common mistake

    Assuming a shared_ptr is destroyed when the first copy goes out of scope

    What it looks like

    A copy is made into a helper function, and the helper returns while the original is still in use.

    Why it fails

    Each copy is an owner. The object is destroyed only when the last shared_ptr that owns it is destroyed.

    Do this instead

    Count the owners. Destruction happens when the count reaches zero.

  • Common mistake

    Writing a constructor that acquires a resource but no destructor to release it

    Why it fails

    Nothing in the class's own code returns the resource, so it stays held after the object is gone.

    Do this instead

    Pair every acquisition in the constructor with a release in the destructor.

  • Common mistake

    Using naked new with a delete that only runs on the normal path

    What it looks like

    An early return in a function skips the delete at the bottom.

    Why it fails

    Every naked new must be matched by a delete on every path through the code.

    Do this instead

    Hold the resource in a handle or smart pointer so release happens when the object is destroyed.

  • Common mistake

    Choosing shared_ptr by default when a single owner would make the lifetime clearer

Initializer-List Constructors for Containers

A container is a class that holds a collection of elements. The guideline is that a container class should be given an initializer-list constructor. This constructor accepts a list of values written in braces, so a user can create the container already filled with those values. This makes the container easier to create and read, and it matches the way programmers commonly write small collections of values.

cpp
Notes on the code
  • The braced list is passed to the constructor as an initializer list
  • The container's elements come straight from the braced list

The example shows only the general shape of such a constructor. The braced values fill the container at creation time, which is the property the guideline is about.

Checking Your Understanding of Shared Lifetimes

MEDIUM

Three shared_ptr copies p, q, and r point to one object. The copy q is destroyed first, then p, then r. After which destruction does the object get destroyed?

Hints
  • Count the owners after each destruction, starting from three.
  • The object is destroyed only at the step where no owners remain.
MEDIUM

A function starts a background task that holds a copy of a shared_ptr to a file handle, then returns. Where would you look to find out how long the file stays open, and why does the answer depend on the task rather than the function?

Hints
  • The lifetime is set by the longest-living owner.
  • Think about which owners are still alive after the function returns.

Key Takeaways

  • A shared_ptr lets several owners share one object. Each copy is a new owner, and the object is destroyed only when the last shared_ptr that owns it is destroyed.
  • Use shared_ptr only when shared ownership is truly needed. It makes the lifetime harder to predict, so a single owner is usually the simpler design.
  • A constructor that acquires a resource creates a duty, and the class needs a destructor to release it. RAII and resource handles tie the release to the object's lifetime so it happens on every path.
  • Naked new and delete put the burden of matching every release on the programmer. Managed alternatives make the release automatic.
  • make_shared() creates an object and its shared_ptr together without changing the ownership rules. Container classes should provide an initializer-list constructor.

Related concepts

More concepts starting with D