Chapter 11 of 24 | C++
Memory and resource management
A resource is anything that must eventually be released or finalized: dynamically allocated memory, a file handle, a lock, or a network connection. C++'s core resource-management technique is RAII: acquire a resource in an object's constructor and release it in that object's destructor. Scope then handles cleanup on both normal returns and exceptions.
Prefer automatic and standard-library ownership
#include <vector>
void process()
{
std::vector<int> numbers;
numbers.push_back(4);
numbers.push_back(9);
// The vector releases its storage automatically here.
}
Local objects are destroyed in reverse order of construction. Standard containers, strings, and file streams manage their own resources. Prefer them to manual calls to new and delete.
Unique ownership
#include <memory>
auto item = std::make_unique<int>(42);
std::cout << *item << '\n';
std::unique_ptr expresses sole ownership: when the pointer leaves scope, its object is destroyed. It cannot be copied, but it can be moved to transfer ownership. std::make_unique is the preferred way to construct one when dynamic lifetime is actually needed.
Shared ownership is a deliberate choice
std::shared_ptr keeps an object alive while at least one shared owner remains. It has bookkeeping cost and can obscure who is responsible for a lifetime. Use it only when ownership is genuinely shared; cycles of shared pointers can keep objects alive forever. A std::weak_ptr can observe a shared object without extending its lifetime and can help break cycles.
Make resource-owning types safe
A type that directly manages a resource must handle copying, moving, and destruction consistently. A common rule of thumb is the Rule of Zero: compose standard-library resource-owning members and let their special member functions do the work. If you need custom ownership logic, study the Rule of Five and make copy and move behavior explicit.
Manual new/delete is rarely needed in application code. Mismatched allocation and release, early returns, exceptions, and duplicated ownership can all cause leaks or double deletion.
RAII makes cleanup automatic
Consider a function that opens a file, reads some data, and then discovers invalid input. If cleanup is a separate call at the bottom of the function, every early return must remember to close the file. If reading throws an exception, that manual cleanup might never execute. A file-stream object solves the problem because its destructor closes the file whenever the object leaves scope.
The same pattern applies to locks, temporary directories, and other resources. A well-designed owner acquires in construction, releases in destruction, and avoids exposing half-managed states. This is why a local standard-library object is often safer than a raw resource handle plus a cleanup convention that every caller must remember.
Copying and moving ownership
Copying a vector creates a separate vector with its own elements. Moving a vector transfers its resources into a new owner when possible, leaving the original object valid but in an unspecified state. The compiler and standard library use move operations when appropriate, so you can often return a local container directly without manually writing ownership-transfer code.
std::unique_ptr follows this sole-owner model. Since two unique pointers must not independently delete the same object, copying is prohibited. Moving communicates that ownership is transferred. A function may accept a std::unique_ptr<T> by value to take ownership, return one to give ownership to the caller, or take it by reference when it only needs to reseat the caller's owner.
When shared ownership is and is not appropriate
A shared pointer uses a shared control block to keep track of owners. The object is destroyed after the last owning shared pointer is released. This convenience comes with memory and runtime overhead, and the ownership graph can become difficult to understand. Prefer a single clear owner whenever possible, then pass references or pointers for temporary access.
Shared ownership is reasonable when independent parts of a program genuinely need to keep an object alive for different lengths of time. A weak pointer is a non-owning observer of shared ownership; it can be checked and temporarily promoted to a shared pointer if the object is still alive. It does not keep the target alive by itself.
Exception safety and the Rule of Zero
RAII is important when operations fail. If a constructor or function throws, already-constructed local objects are destroyed as the stack unwinds. This releases their resources. A type composed of standard members can often rely on compiler-generated copy, move, and destruction operations; this is the Rule of Zero.
If a class directly owns a raw resource and must define a destructor, it may also need to define or disable copying and provide move operations. This is the Rule of Five. Correctly implementing all these operations is subtle, so prefer a standard owner as a member rather than writing manual resource management.
Practice
- Replace a manually managed dynamic array with
std::vector. - Use a local
std::ifstreamand explain why it closes on scope exit. - Decide whether each object should be stored by value, uniquely owned, or shared; justify the lifetime requirement.
- Trace which destructors run when a function returns early from inside a nested scope.
- Return a vector from a function and verify that no manual allocation is required.
- Draw a shared-ownership graph and identify whether any cycle would keep objects alive.
Key idea: bind resource lifetime to object lifetime, and make ownership explicit in types.
Allocation, construction, and destruction are distinct
Dynamic storage allocation obtains a region of memory; object construction starts an object's lifetime in suitable storage; destruction ends that lifetime; deallocation releases the storage. The expression new T combines allocation and construction, while delete normally combines destruction and deallocation. Separating these concepts is important when reasoning about custom allocators, arenas, placement construction, and containers.
Application code should normally use containers and smart pointers rather than call raw allocation functions. Standard containers know how to construct and destroy elements, grow storage, and release allocations. If a custom allocator or memory resource is needed, the design must still respect alignment, object lifetime, exception safety, and the requirement to release storage through a compatible mechanism.
Exception-safety guarantees
The no-throw guarantee means an operation will not propagate an exception. The basic guarantee preserves invariants and avoids leaks, but the object's value may have changed. The strong guarantee provides commit-or-rollback behavior: if the operation fails, observable state remains unchanged. The no-fail guarantee is stronger in wording and is often expected of destruction and move operations used during cleanup.
These guarantees influence composition. A function can build a new value in a local RAII owner, perform all potentially failing work, then commit with a non-throwing swap. This “prepare, then commit” pattern can provide the strong guarantee. A destructor should not let an exception escape during stack unwinding; cleanup failures need an explicit policy rather than a second exception replacing the original failure.
Allocator-aware and polymorphic memory resources
C++17's polymorphic memory-resource facilities allow certain standard containers to use a runtime-selected allocation strategy. A monotonic resource can be useful when many objects share one phase-based lifetime and the entire arena is released together. Pool resources can suit repeated allocations of similar sizes. These strategies can reduce allocation overhead, but they do not extend object lifetimes or automatically make references stable.
A memory resource must outlive containers and objects that allocate from it. Destroy those objects before releasing the resource. Arena allocation can retain memory longer than individual objects need, so measure peak memory as well as speed. A specialized allocator is a performance and lifetime design decision, not a general replacement for standard ownership.
Concurrency and ownership
Shared ownership does not make an object's mutable state thread-safe. A shared pointer's control block supports ownership bookkeeping, but concurrent unsynchronized access to the pointed-to object can still be a data race. Protect shared mutable state with a synchronization design, or transfer ownership so one thread has exclusive access.
Advanced exercise
- Compare the strong and basic exception guarantees for a vector-like operation that may allocate.
- Use a monotonic resource for a short-lived batch, ensuring all dependent objects are destroyed before the resource.
- Draw a resource-lifetime graph for a function that owns a file, a container, and a view into that container.
- Explain why reference-counted ownership does not by itself prevent data races.