Concepts / Resource Management with RAII

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.

  • Programming

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.

cpp
Output
test(): bad size, logging and passing it on
main() handled: negative size
Notes on the code
  • 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_alloc and terminates instead, because test() 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.

main()outer handler waits heretest()first handler catches,logs, rethrowsVector(-1)throws out_of_rangecalls test(-1)constructs Vectorexception thrownbare throw; rethrows
When test() rethrows with bare throw;, which frames are exited, which handler sees the exception next, and where does control land?

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()?

  • It would be rethrown to main() exactly as before

  • It would be caught and silently ignored

  • The program would terminate immediately inside test()

Reveal answer
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.

cpp
Notes on the code
  • The constructor acquires the heap array
  • The destructor releases the same array
  • Callers never write a delete for the array themselves
Worked example

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?

  1. 1
    Construction

    Buffer(n) runs and allocates the array, so the object now holds the resource.

  2. 2
    Work

    The function uses the buffer. Nothing in the calling code needs to remember the array.

  3. 3
    Exception leaves scope

    The exception exits the function early. The local Buffer, which is fully constructed, is destroyed as part of leaving the scope.

  4. 4
    Release

    The destructor runs and deletes the array. The release happens on this path even though the function never reached its last line.

Result

The resource is released because it is attached to the object's end, not to a line of code that might be skipped.

CallerBuffer objectHeap array1. construct Buffer(n)2. allocate array3. use buffer4. scope ends or exception leaves5. destructor deletes array
In what order do the constructor, the work, and the destructor run, and when is the resource released if an exception leaves the scope early?

RAII and Resource Handles

Definition

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.

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

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

After
void work(int n) {
    Buffer b(n);          // acquired in the constructor
    do_something(b);      // may throw
}                         // destructor releases on every exit

The release is attached to the object, so it happens whether the function returns normally or an exception leaves early.

new int[n]Buffer constructedexception thrownexception throwndelete never reacheddestructor releasesarray
If an exception is thrown between new and delete, what happens to the allocated memory with a raw pointer, and how does a managed handle change that outcome?

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.

cpp
Notes on the code
  • The braced list {3, 1, 4} becomes the initializer_list argument
  • 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 fails

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

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

    A handler logs an error and returns normally, so the caller believes the work succeeded.

    Why it fails

    A handler that only does local work and does not recover should rethrow, otherwise the caller never learns that anything went wrong.

    Do this instead

    Decide 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 fails

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

    Do this instead

    Pair 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 fails

    An exception or early return can leave the function before the delete runs, and the memory is lost.

    Do this instead

    Hold 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

MEDIUM

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.

Related concepts

More concepts starting with R