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.
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?
Reveal 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.
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.
make_sharedcreates 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
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.
| Question | Single owner | Shared ownership with shared_ptr |
|---|---|---|
| Who decides when the resource is released? | The one owner, when it is destroyed | The last owner to be destroyed |
| Where is the lifetime visible? | In one place, the owner's scope | Across every place that holds a copy |
| How is it passed along? | Moved, with one holder at a time | Copied, with each copy a new owner |
| When is it the better fit? | One part of the program is clearly in charge | Several 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.
std::shared_ptr<Resource> p(new Resource());The object is created with a separate new expression and then handed to the shared_ptr.
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.
- 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.
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.
- 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
- The managed version attaches the release to the object's scope
- Every path out of the function releases the array without a manual delete
Common mistake Assuming a shared_ptr is destroyed when the first copy goes out of scope
What it looks likeA copy is made into a helper function, and the helper returns while the original is still in use.
Why it failsEach copy is an owner. The object is destroyed only when the last
shared_ptrthat owns it is destroyed.Do this insteadCount 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 failsNothing in the class's own code returns the resource, so it stays held after the object is gone.
Do this insteadPair 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 likeAn early return in a function skips the delete at the bottom.
Why it failsEvery naked new must be matched by a delete on every path through the code.
Do this insteadHold 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.
- 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
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.
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_ptrlets several owners share one object. Each copy is a new owner, and the object is destroyed only when the lastshared_ptrthat owns it is destroyed. - Use
shared_ptronly 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 itsshared_ptrtogether without changing the ownership rules. Container classes should provide an initializer-list constructor.