Last updated:2026-10-12
C++ RAII Resource Management¶
0. Introduction¶
In C, there are three kinds of memory allocation: static, automatic, and dynamic. Static variables are constants embedded in the source file; because they have a known size and never change, they are not particularly interesting. Automatic allocation can be thought of as stack allocation — space is allocated when a lexical block is entered and freed when the block exits. Its most important characteristic is directly related to this. Before C99, automatically allocated variables needed to have a size known at compile time. This meant that any strings, lists, maps, and any structures derived from these had to live in dynamic memory on the heap.
Programmers explicitly allocate and free dynamic memory with four basic operations: malloc, realloc, calloc, and free. The first two perform no initialization, and the memory may contain garbage. Apart from free, they can all fail. In that case they return a null pointer, and accessing it is undefined behavior; in the best case, your program crashes. In the worst case, your program appears to work for a while, processing garbage data before crashing.
For example:
int main() {
char *str = (char *) malloc(7);
strcpy(str, "toptal");
printf("char array = \"%s\" @ %u\n", str, str);
str = (char *) realloc(str, 11);
strcat(str, ".com");
printf("char array = \"%s\" @ %u\n", str, str);
free(str);
return(0);
}
Output:
Although the code is simple, it already contains an anti-pattern and a questionable decision. In real life, you should not write the number of bytes directly; you should use the sizeof function instead. Similarly, we allocate the char * array to be exactly twice the size of the string we need (double the string length, to account for the null terminator), which is a rather expensive operation. A more sophisticated program might build a larger string buffer, allowing the string to grow.
1. The Invention of RAII: A New Hope¶
To put it mildly, all manual management is unpleasant. In the mid-80s, Bjarne Stroustrup invented a new paradigm for his brand-new language C++. He called it "resource acquisition is initialization", and its essential insight is this: you can specify that an object has constructors and a destructor, which the compiler calls automatically at the appropriate times, which provides a far more convenient way to manage the memory of a given object when needed, and the technique is also useful for resources that are not memory.
This means the example above becomes more concise in C++:
int main() {
std::string str = std::string ("toptal");
std::cout << "string object: " << str << " @ " << &str << "\n";
str += ".com";
std::cout << "string object: " << str << " @ " << &str << "\n";
return(0);
}
Output:
In the example above, there is no manual memory management! The string object is constructed, overloaded methods are called, and it is destroyed automatically when the function exits. Unfortunately, that same simplicity leads to other problems. Let's look at an example in detail:
vector<string> read_lines_from_file(string &file_name) {
vector<string> lines;
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines.push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
int count = read_lines_from_file(file_name).size();
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
Output:
This looks simple. The vector is filled in, returned, and accessed. However, as performance-minded, efficient programmers, some aspects of it bother us: in the return statement, because value semantics are used, the vector is copied into a new vector shortly before being destroyed.
In modern C++, this is no longer a strict requirement. C++11 introduced the concept of move semantics, in which the origin is left in a valid (so that it can still be destroyed correctly) but unspecified state. A return from a call is the easiest case for the compiler to optimize into a move, because it knows that the source will be destroyed shortly before any further access. The point of this example, however, is to illustrate why people invented a whole slew of garbage-collected languages in the late 80s and early 90s, when C++ move semantics were not available.
For files with large amounts of data, this can become expensive. Let's optimize it to return only a pointer. The syntax changes slightly, but the rest of the code is the same:
vector<string> * read_lines_from_file(string &file_name) {
vector<string> * lines;
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines->push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
int count = read_lines_from_file(file_name).size();
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
Output:
The program crashes! We simply need to allocate memory for lines above:
Now it runs!
Unfortunately, although this looks perfect, it still has a flaw: it leaks memory. In C++, a pointer to the heap must be manually deleted once it is no longer needed; otherwise, as soon as the last pointer goes out of scope, the memory becomes unusable and is only reclaimed when the operating system cleans it up at the end of the process. Idiomatic modern C++ would use unique_ptr here, which implements the desired behavior. It deletes the object the pointer points to when the pointer goes out of scope. However, this behavior only became part of the language in C++11.
Here, we can keep using the pre-C++11 syntax and just change main:
vector<string> * read_lines_from_file(string &file_name) {
vector<string> * lines = new vector<string>;
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines->push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
vector<string> * file_lines = read_lines_from_file(file_name);
int count = file_lines->size();
delete file_lines;
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
Memory is allocated and released manually.
Unfortunately, as the program grows beyond the scope above, it quickly becomes much harder to reason about when and where the pointer should be deleted. When a function returns a pointer, do you now own it? Should you delete it yourself once you are done, or does it belong to some data structure that will be freed all at once later? Err on one side and you leak memory; err on the other side and you have corrupted the data structure in question and possibly other data structures, because they try to dereference pointers that are no longer valid.
2. "Use the garbage collector, flyboy!"¶
Garbage collectors are not a new technology. They were invented by John McCarthy in 1959 for Lisp. In 1980, with the arrival of Smalltalk-80, garbage collection started to go mainstream. But the 1990s represent the technology's real flowering: between 1990 and 2000, a whole set of languages were released, all using one kind of garbage collection or another: Haskell, Python, Lua, Java, JavaScript, Ruby, OCaml, and C# are the best known.
What is garbage collection? In short, it is a set of techniques for automating manual memory management. It is usually provided as a library for languages with manual memory management (such as C and C++), but it is more commonly used in languages that require it. The biggest advantage is that the programmer does not have to think about memory at all. It is all abstracted away. For example, the Python equivalent of our file-reading code above looks like this:
def read_lines_from_file(file_name):
lines = []
with open(file_name) as fp:
for line in fp:
lines.append(line)
return lines
if __name__ == '__main__':
import sys
file_name = sys.argv[1]
count = len(read_lines_from_file(file_name))
print("File {} contains {} lines.".format(file_name, count))
The array of lines springs into existence at its first assignment, and it is returned without being copied into the calling scope. Because the timing is non-deterministic, it is cleaned up by the garbage collector at some point after it goes out of scope. Interestingly, RAII for non-memory resources is not idiomatic in Python. It is allowed — we could simply write fp = open(file_name) instead of using a with block and let the GC clean up — but the recommended pattern is to use a context manager where possible so that resources can be released at a deterministic time.
Although memory management is simplified, it comes at a significant cost. In reference-counting garbage collection, every variable assignment and scope exit incurs a small cost to update the references. In a mark-and-sweep system, the execution of the whole program pauses at unpredictable intervals while the GC sweeps memory. This is commonly called a stop-the-world event. Implementations like Python, which use both systems at once, suffer both penalties. These issues reduce the suitability of garbage-collected languages for situations where performance is critical or real-time behavior is required. Even on the following toy program, the actual performance drop is visible:
$ make cpp && time ./c++ makefile
g++ -o c++ c++.cpp
File makefile contains 38 lines.
real 0m0.016s
user 0m0.000s
sys 0m0.015s
$ time python3 python3.py makefile
File makefile contains 38 lines.
real 0m0.041s
user 0m0.015s
sys 0m0.015s
The real time of the Python version is nearly three times that of the C++ version. Although not all of this difference can be attributed to garbage collection, it is still considerable.
3. Ownership: RAII Awakens¶
We know that the lifetime of an object is determined by its scope. Sometimes, however, we need to create an object that is independent of the scope in which it was created — which is useful, or even very useful. In C++, the operator new is used to create such an object. To destroy the object, the operator delete can be used. Objects created by the operator new are dynamically allocated, i.e. allocated in dynamic memory (also known as the heap or the free store). Consequently, an object created by new continues to exist until it is explicitly destroyed with delete.
Some of the errors that can occur when using new and delete are:
-
Object (or memory) leak: an object is allocated with new, and you forget to delete it.
-
Premature deletion (or dangling reference): you hold another pointer to the object, delete the object, while yet other pointers still refer to it.
-
Double deletion: attempting to delete an object twice.
Scoped variables are generally preferred. However, RAII can be used as an alternative to new and delete to make an object outlive its scope. This technique consists of allocating a pointer to an object allocated on the heap and putting it inside a handle/manager object. The latter has a destructor that takes care of destroying that object. This ensures that the object is available to any function that wants to access it, and that the object is destroyed at the end of the handle object's lifetime, without any explicit cleanup.
Examples of RAII usage from the C++ standard library are std::string and std::vector.
Consider this code:
void fn(const std::string& str)
{
std::vector<char> vec;
for (auto c : str)
vec.push_back(c);
// do something
}
When you create a vector and push elements into the vector, you do not have to worry about allocating and deallocating memory for those elements. The vector uses new to allocate space for its elements on the heap and delete to free that space. As a user of vector, you do not need to care about the implementation details, and you trust that the vector does not leak. In this case, the vector is the handle object for its elements.
Other examples of RAII in the standard library are std::shared_ptr, std::unique_ptr, and std::lock_guard.
Another name for this technique is SBRM, short for scope-bound resource management.
Now let's modify the file-reading example above:
#include <iostream>
#include <vector>
#include <cstring>
#include <fstream>
#include <bits/unique_ptr.h>
using namespace std;
unique_ptr<vector<string>> read_lines_from_file(string &file_name) {
unique_ptr<vector<string>> lines(new vector<string>);
string line;
ifstream file_handle (file_name.c_str());
while (file_handle.good() && !file_handle.eof() && file_handle.peek()!=EOF) {
getline(file_handle, line);
lines->push_back(line);
}
file_handle.close();
return lines;
}
int main(int argc, char* argv[]) {
// get file name from the first argument
string file_name (argv[1]);
int count = read_lines_from_file(file_name).get()->size();
cout << "File " << file_name << " contains " << count << " lines.";
return 0;
}
4. Only at the End Do You Realize the True Power of RAII.¶
Since the invention of compilers, manual memory management has been a nightmare that programmers have tried to avoid. RAII is a promising pattern, but without some weird workarounds it simply could not be used for heap-allocated objects, and so C++ suffered. The 90s consequently saw an explosion of garbage-collected languages, aiming to make programmers' lives more pleasant, even at the cost of performance.
Finally, RAII can be summarized as follows:
-
Resources are released in the destructor
-
Instances of the class are stack-allocated
-
Resources are acquired in the constructor.
RAII stands for "resource acquisition is initialization".
Common examples are:
-
File operations
-
Smart pointers
-
Mutexes
5. Reference Articles¶
1. https://www.toptal.com/software/eliminating-garbage-collector#remote-developer-job
Related Topics¶
- C++ Avoiding the Default Constructor | C++那些事
- C++ Class Initializer List Idiom | C++那些事
- C++ Prefer Class Templates over Function Templates | C++那些事
- C++ copy-and-swap Idiom | C++那些事
- C++ Dependency Injection | C++那些事
- C++ enum class Strongly Typed Enums | C++那些事
- C++ Header-Only vs Implementation Separation | C++那些事
- C++ Lazy Evaluation Idiom | C++那些事
- C++ Lookup Table Idiom | C++那些事
- C++ Member Pointers and Type Information | C++那些事