RAII and Scope-Based Resource Management
A resource handle is an object that manages a resource, and it is distinct from the resource itself.
Why Resources Need a Responsible Owner
A program often obtains a resource, such as memory, a file, or some other limited item, and must later give it back. If the program forgets to give it back, or gives it back in the wrong place, the resource stays in use longer than it should. This article is about a way of making that obligation reliable, so that the programmer does not have to track every release by hand.
Managing that obligation by hand is error-prone. The code path that acquires a resource and the code path that releases it can be far apart in the program, and early exits can skip the release entirely. The approach taught here is to let an object carry the obligation, so that the release is tied to the object's lifetime instead of to a line of code that might be missed.
Tracing a Handle From Declaration to Scope Exit
Consider a resource handle declared inside a block. A resource handle is an object whose job is to manage a resource, and it is distinct from the resource itself. Its lifecycle has a clear sequence of states. First, the handle is created, and as part of that creation it acquires the resource it manages. Second, the program works with the resource through the handle for as long as the handle exists. Third, the program leaves the block, the handle goes out of scope, and it is destroyed. Fourth, that destruction releases the resource.
Read the timeline from left to right. The acquisition happens at the start, when the handle is created, and not at some later line the programmer has to remember. The release happens at the end, when the handle is destroyed as its scope ends. Tracing the handle's lifetime from creation through scope exit therefore tells you exactly when the resource is given back.
The practical payoff is that release becomes a consequence of the handle's lifetime ending. The programmer does not need a separate release statement at every exit point, because the handle's lifetime already determines when the release happens.
How RAII Ties Release to Destruction
RAII stands for Resource Acquisition Is Initialization. Under this idiom, a resource is acquired during the initialization of an object, and because of that, the object's lifetime controls the resource. The acquisition is part of creating the handle, and the release follows from the handle's destruction when its scope ends.
The name describes the pattern in three parts. Acquisition happens at creation. Initialization is the moment the object comes into existence. Because the object's lifetime is what governs the resource, the object's destructor is the place where the release is performed. The source material puts the idea in one sentence: RAII ties a resource's lifetime to an object's lifetime, so the object's destructor performs the release.
A resource handle is the object that puts this idiom to work. Its job is to hold a resource on behalf of the code that uses it. The calling code uses the resource through the handle, while the handle keeps the responsibility for the resource. The diagram below shows the two separate things involved: the handle, which lives on the stack, and the resource it owns, which lives elsewhere.
The handle is the piece responsible for releasing the resource. The calling code never needs to know when or how the resource is returned, because the handle owns that decision. Requests and values travel from the calling code into the handle, which forwards them to the resource, and results travel back the same way.
A Constructor That Takes On a Duty
A class often needs something from outside itself to do its job, such as memory, a file, a lock, or another 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 source states the guideline directly: 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 sequence below shows the order of these events.
Notice what the sequence does not contain: any step where the programmer must remember to release the resource by hand. When the destructor is written, the release is attached to the object's end rather than to a line of code that might be skipped. The following class is a small example written for this article. Its constructor acquires an array on the heap, and its destructor releases it. The acquisition happens in the constructor, so the release belongs in the destructor.
class IntBuffer {
public:
IntBuffer(int n) : data_(new int[n]), size_(n) {}
~IntBuffer() { delete[] data_; }
int size() const { return size_; }
private:
int* data_;
int size_;
};
int main() {
IntBuffer buf(8);
// use buf.size() and the heap array
return 0;
}- 1
IntBuffer(int n)runs when buf is declared and acquires an array of n integers on the heap. - 2The member initializer stores the pointer in data_ and the size in size_.
- 3~
IntBuffer()runs automatically when buf goes out of scope and calls delete[] on data_. - 4main never releases the array itself, because the object's destructor is responsible for that duty.
- The constructor acquires the heap array with new
- The destructor releases that same array with delete[]
- The calling code never writes a delete statement
Early Returns and Exceptions
The benefit of tying release to destruction matters most when a function does not follow its ordinary path. A function may return early, or an exception may be raised between the acquisition and the point where the programmer would normally release the resource. The flowchart below follows both branches. Notice that the managed path always ends with the destructor running, while the raw pointer path can end without any release at all.
Naked new and delete Versus a Managed Handle
The source advises avoiding naked new and delete operations. 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. The burden of matching every release is placed on the programmer, which is why the guideline exists and why that burden is easy to underestimate.
The managed alternative provides a handle whose acquisition and release are both attached to the object. The programmer writes the acquisition as part of creating the handle and does not write a matching release at each exit. The leak point in the naked version, the place where a delete can be skipped, does not exist in the handle version, because the release is driven by the handle's destruction.
Mistakes That Undermine Handle Management
Common mistake Writing a constructor that acquires a resource but no destructor to release it
What it looks likeA class whose constructor allocates an array with new but which has no destructor
Why it failsNothing in the class's own code returns the resource, so it stays held after the object is gone.
Do this insteadWrite a destructor that releases the resource the constructor acquired, as the IntBuffer example does.
Common mistake Relying on a separate release statement at the end of a function
What it looks likeCalling delete on a pointer near the bottom of a function body
Why it failsAn early return or an exception can jump past that line, so the release is skipped.
Do this insteadLet the handle's destructor perform the release, so it runs when the scope ends on any path.
Common mistake Assuming the calling code must release the resource
What it looks likeCalling code that expects to issue the release itself after using a handle
Why it failsThe source makes the handle responsible for release, so the calling code's job is to use the resource through the handle.
Do this insteadTrace the handle's lifetime and expect the release at its destruction.
Common mistake Declaring a handle in a wider scope than needed, so the resource is held longer than the work requires
Common mistake Confusing the handle with the resource it manages, when the handle is the object that owns the release
A function acquires a resource, then has a branch that returns before the release line. The resource stays in use after the function exits. Tracing the path through the branch shows that the release was never reached. Rewriting the resource as a handle moves the release into the handle's destructor, so the early return no longer matters.
Initializer-List Constructors for Containers
The last guideline in this material 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 supplies the elements all at once
- The container is populated at the moment it is created
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 pack does not describe further details of this constructor, so this example shows only the general shape of its use and makes no further claims about how it is implemented.
Practice Tracing a Handle's Lifetime
A block declares a resource handle, then uses it, then leaves the block. Write out the order of four events: the handle is created, the resource is acquired, the resource is released, and the handle is destroyed. Then say which event would move if the programmer added an early return between the use and the end of the block.
Hints
- Acquisition is part of creating the handle, so it happens at declaration.
- Release follows from the handle's destruction when its scope ends.
- An early return still leaves the block, so the handle is still destroyed on the way out.
Explain, in your own words, why a class whose constructor acquires a resource needs a destructor that releases it, and what would happen to the resource if the destructor were missing.
Hints
- Think about who gives the resource back.
- Consider what remains held after the object is gone.
Key Takeaways
- A resource handle is an object that manages a resource, and it is distinct from the resource itself.
- Under RAII, resource acquisition happens during creation of the handle, and release follows from the handle's destruction when its scope ends.
- Tracing a handle from declaration through scope exit tells you exactly when its resource is given back, including on early returns and exceptions.
- A constructor that acquires a resource creates a duty, so the class needs a destructor that releases it; naked new and delete put the burden of matching every release on the programmer.
- A container class should provide an initializer-list constructor so that a user can create it already filled with a braced list of values.