Resource Management with RAII
The idea that invariants underlie resource management is what makes the constructor and destructor pair so important, and the next section builds on that connection.
When a Function Cannot Finish Its Job
Imagine a function that was asked to build something, such as a container whose size comes from an argument. Partway through the work, something fails and an exception is thrown. The function now has two choices. It can produce a sensible result and return normally, or it can admit that it has no way to finish and let the failure travel back to whoever called it. Both choices are legitimate, and learning to tell them apart is the first step in understanding how C++ programs manage failure and the resources they hold.
This article follows one function through its failure path. As you read, keep a running prediction: when the exception is thrown, which code runs next, and what happens to the work the function already did? Trace the path yourself, and check your answer against the explanation that follows.
Recovering Versus Rethrowing
Once an exception is thrown, normal statement-by-statement execution stops. Control leaves the current sequence of statements and searches for a handler that matches the exception's type. A handler that catches the exception and then executes throw; with no operand does not create a new exception. It re-raises the one it is currently handling. Control therefore leaves the handler and the function, and the search for a suitable handler continues in the function that called this one. The exception keeps travelling up the chain of calls until some handler accepts it or the program ends.
The word handling covers two different outcomes. Sometimes a handler finishes the job: the problem is resolved and the function returns normally, so the caller never learns that anything went wrong. Other times a handler does only a small amount of local work, such as logging a message or releasing something the function had acquired, and then rethrows. In that second case the function has no way to complete its assigned task, so it passes the failure onward so that a caller with more context can decide what to do.
test(): bad size, logging and passing it on
main() handled: negative size- Vector's constructor throws when the size is negative
- The first handler matches
out_of_range, logs a message, then uses bare throw; to rethrow - The second handler matches
bad_allocand terminates instead, becausetest()has no sound way to continue main()has the final handler that accepts the rethrown exception
Read the example as a path, not as a list. The exception originates in the Vector construction, and the search begins in test(). The first handler matches, so the message is printed before the rethrow. That ordering matters: the function has done its own bookkeeping before passing the failure along. The bare throw; then leaves test(), and the search for a handler resumes in main(), which accepts it.
What do you think happens?
In the example, if the second handler (for bad_alloc) were replaced by a bare throw;, what would happen to a bad_alloc exception that reached test()?
Reveal answer
It would be rethrown to main(), and main() has no handler for bad_alloc, so the search would continue past main() and nothing in this program would accept it.
A rethrow keeps the same exception and continues the search in the caller. Whether a handler exists above is what decides the outcome. The original code chose std::terminate because test() judged that it had no reasonable way forward.
Choosing Between Rethrowing and Terminating
The second handler in the example shows the opposite decision. A std::bad_alloc signals memory exhaustion, and the code comment says that test() is not designed to handle that situation. Rather than rethrow, the handler calls std::terminate, which ends the program. Choosing between rethrowing and terminating is a judgment about whether the current function has any reasonable way forward. Rethrowing hands the decision to someone else; terminating says that no one should continue.
Class Invariants as Design Contracts
The rethrowing example is about what a function does when things go wrong. Invariants are about what must always be true. An invariant is a condition that remains true during the execution of a program, and the source treats invariants as central to the design of classes. Functions have a related idea called preconditions, which describe what must hold before a function is called. Invariants describe what a class must keep true throughout its life.
- Stating an invariant helps you understand precisely what you want the class to guarantee.
- It forces you to be specific about those guarantees.
- Specificity makes it more likely that the code is correct once you have debugged and tested it.
Invariants also connect directly to how C++ manages resources. A constructor establishes the invariant when an object comes into existence, and a destructor cleans up when the object's lifetime ends. The idea that invariants underlie resource management is what makes the constructor and destructor pair so important, and the next section builds on that connection.
Constructors Acquire and Destructors Release
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. The guideline in the source 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.
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 the resource back. Between those two points the object can use the resource freely.
- The constructor acquires the heap array
- The destructor releases the same array
- Callers never write a delete for the array themselves
Tracing a Buffer's lifetime
A function creates a Buffer local to its body, then an exception leaves the function before it reaches its final line. What happens to the array?
- 1Construction
Buffer(n)runs and allocates the array, so the object now holds the resource. - 2Work
The function uses the buffer. Nothing in the calling code needs to remember the array.
- 3Exception leaves scope
The exception exits the function early. The local Buffer, which is fully constructed, is destroyed as part of leaving the scope.
- 4Release
The destructor runs and deletes the array. The release happens on this path even though the function never reached its last line.
The resource is released because it is attached to the object's end, not to a line of code that might be skipped.
RAII and Resource Handles
RAII stands for Resource Acquisition Is Initialization. A resource is acquired during initialization of an object, and because of that, the object's lifetime controls the resource. RAII ties a resource's lifetime to an object's lifetime, so the object's destructor performs the release.
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. The calling code uses the resource through the handle, 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.
Naked new and delete Versus a Managed Handle
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. Naked new and delete operations put the burden of matching every release on the programmer, which is why the guideline is to avoid them. That burden is easy to underestimate, especially when an exception can leave the function between the allocation and the release.
void work(int n) {
int* p = new int[n];
do_something(p); // may throw
delete[] p; // skipped if do_something throws
}The array is released only if the delete line is reached.
void work(int n) {
Buffer b(n); // acquired in the constructor
do_something(b); // may throw
} // destructor releases on every exitThe release is attached to the object, so it happens whether the function returns normally or an exception leaves early.
Initializer-List Constructors for Containers
The last guideline in this part concerns container classes. A container is a class that holds a collection of elements. The source says that if a class is a container, it should be given an initializer-list constructor. An initializer-list 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 {3, 1, 4} becomes the
initializer_listargument - The constructor copies each element into storage it owns
- The destructor releases that storage, following the same RAII pattern
In this sketch the container's elements come straight from the braced list, so the object is populated at the moment it is created. The source does not describe further details of this constructor, so the example shows only the general shape.
Mistakes Beginners Make with Rethrow and Resources
Common mistake Writing throw ex; instead of throw; inside a handler
Why it failsThe bare throw; re-raises the exception currently being handled. Naming the object in the throw creates a new throw expression, which is not what the guideline describes.
Do this insteadInside the handler, use throw; with no operand when you want to propagate the current exception.
Common mistake Catching an exception and then falling through, assuming the function will still complete its task
What it looks likeA handler logs an error and returns normally, so the caller believes the work succeeded.
Why it failsA handler that only does local work and does not recover should rethrow, otherwise the caller never learns that anything went wrong.
Do this insteadDecide whether the function can truly recover. If not, rethrow so the caller can decide.
Common mistake Giving a class a constructor that acquires a resource but no destructor
Why it failsNothing in the class returns the resource, so it stays held after the object is gone.
Do this insteadPair every resource-acquiring constructor with a destructor that releases the resource.
Common mistake Pairing a naked new with a delete that is skipped on some path
Why it failsAn exception or early return can leave the function before the delete runs, and the memory is lost.
Do this insteadHold the resource in an object whose destructor performs the release.
Common mistake Assuming that a rethrown exception is handled once it leaves the current function
Practice: Trace a Failure Path
A function create() constructs a Buffer local to its body and then calls a helper that throws std::out_of_range. create() has a handler for out_of_range that prints a message and then executes bare throw;. Main() has a handler for out_of_range as well. In order, list what happens: where the Buffer's destructor runs, what the handler in create() does, and where the exception is finally caught.
Hints
- Think about which objects are fully constructed when the exception leaves
create(). - The bare throw; leaves
create()and continues the search in the caller. - The destructor of the local Buffer runs as the exception leaves the scope.
Key Takeaways
- A handler that cannot complete its function's task can rethrow the current exception with bare throw;, which leaves the function and lets a caller's handler decide.
- A handler that recovers ends the failure inside the function; a handler that only does local work and rethrows passes the decision onward.
- Class invariants state what must always be true for an object, and a constructor establishes them while a destructor cleans up at the end of the object's life.
- RAII ties a resource's lifetime to an object's lifetime, so a resource handle releases its resource in its destructor, including when an exception leaves early.
- Naked new and delete put the burden of matching every release on the programmer, while a managed handle carries that duty automatically; a container should offer an initializer-list constructor.