When asked about one key feature of C++, Bjarne Stroustrup often cites constructors and destructors, or the idea of RAII. RAII, which stands for Resource Acquisition Is Initialization, is a programming idiom that uses the lifetime of objects to manage resources in a safe and efficient manner. A resource is anything that a program acquires from another part of the system and must release back to its owner after use. By tying the acquisition and release of resources to the creation and destruction of objects, RAII helps to prevent resource leaks and other types of errors that can occur when working with resources in C++. In this blog post, we'll explore RAII in more detail, including how it works and why it's such an important concept for C++ developers to understand.
RAII: Resource Acquisition is Initialization
Proper resource management is an important aspect of C++ programming. Failing to properly manage resources such as pointers, file handles, and memory allocations can result in issues like memory leaks, resource leaks, and program crashes. Managing resources can be challenging when functions have multiple return points, as it becomes difficult to ensure that resources are properly cleaned up and released. This can result in resources being left unused, wasting valuable system resources and potentially causing issues with the program. RAII is a technique that helps to address this problem and makes it easier to properly manage resources in C++.
Consider the simple function below that opens a file and extracts a key from it.
Key getKeyFromFile(const char* file_name){
// Open the file as FILE*
FILE* file = std::fopen(file_name, "r");
// Use the file to extract the key
auto key = extractKey(file);
// Close the file
std::fclose(data_file);
return key;
}
And then consider we need to add some additional checks. Specifically, we want to verify that the file is valid before extracting the key, and we want to handle any exceptions thrown by extractKey
function.
Key getKeyFromFile(const char* file_name){
// Open the file as FILE*
FILE* file = std::fopen(file_name, "r");
// Return early if the file is not valid
if (!checkFile(file)){
std::fclose(file); // 1
return {};
}
// Try to extract the key from file
Key key{};
try{
key = extractKey(file);
} catch {
std::fclose(file); // 2
throw;
}
std::fclose(file); // 3
return key;
}
In this updated version of the function, we added three calls to fclose
to handle different scenarios:
If
checkFile
returns false, we close the file and return an empty key.If
extractKey
throws an exception, we close the file and rethrow the exception.If everything goes smoothly, we close the file and return the key.
While this code works fine, it can be somewhat cumbersome to manage multiple calls to fclose
and keep track of the file handle. To simplify the code and avoid potential resource leaks, we can use RAII to automatically close the file when the function returns.
To do this, we can define a FileHandle
class that wraps a FILE*
and closes it when the FileHandle
object goes out of scope.
// RAII style FileHandle
class FileHandle {
public:
// Opens the file as usual and keep it as class member
FileHandle(const char* name, const char* mode) {
f_ = std::fopen(name, mode);
}
// Overload the function call operator to use it as getter to the file
FILE* operator()() const { return file_; }
// Destructor that closes the file
~FileHandle() {
if (f_ != nullptr) {
std::fclose(f_);
}
}
private:
FILE* f_;
};
We can use the FileHandle
class in our getKeyFromFile
function to manage the lifetime of the file handle and ensure that it is properly closed when the function returns. This eliminates the need for multiple calls to fclose
and prevents resource leaks. The revised version of the getKeyFromFile
function would look like this:
Key getKeyFromFile(const char* file_name) {
// Opens the file using FileHandle
FileHandle file(file_name, "r");
// Return early if the file is not valid
if (!checkFile(file())) {
return {};
}
// Try to extract key from file, if it throws
// No try catch, since we don't do handling except closing file.
auto key = extractKey(file());
return key;
}
Smart Pointers
In the past, pointers were a common source of bugs in C++ programs. They required manual management, and it was easy to forget to delete a pointer or double-delete it, leading to memory leaks or other errors. To address these issues, the C++ standard library introduced a class of smart pointers that use RAII to manage pointers automatically.
Smart pointers are one of the most well-known features of C++ that makes use of RAII. They behave like pointers, but they also automatically delete the pointed-to object when they go out of scope. This helps to prevent memory leaks and other errors that can arise from incorrect pointer management. The C++ standard library provides three types of smart pointers:
std::unique_ptr
is designed to provide exclusive ownership of a dynamically-allocated object. This means that aunique_ptr
has sole ownership and control over the object it points to and is responsible for deleting the object when it goes out of scope.std::shared_ptr
is designed to provide shared ownership of a dynamically-allocated object. This means that multipleshared_ptr
instances can point to the same object, and the object will be deleted only when the lastshared_ptr
pointing to it goes out of scope.std::weak_ptr
is designed to hold a non-owning ("weak") reference to a dynamically-allocated object managed by astd::shared_ptr
. This means that a weak_ptr does not participate in reference counting, and does not prevent the object from being deleted when the lastshared_ptr
pointing to it goes out of scope.
Smart pointers are mainly used to manage the lifetime of dynamically allocated objects in C++. They also offer the ability to define custom deleters, making them suitable for managing other types of resources as well. When a smart pointer's lifetime ends, the specified deleter (which can be a function or lambda expression) is called. So they can be useful when you want to manage the lifetime of a resource without creating a separate class for it. For example, we can rewrite getKeyFromFile
example as follows using a unique_ptr
with custom_deleter:
Key getKeyFromFile(Filename file_name) {
// define a custom deleter function to close the file handle
auto closer = [](FILE* file) { std::fclose(file); };
// create a unique_ptr to manage the file handle
std::unique_ptr<FILE, decltype(closer)> file(std::fopen(file_name, "r"), closer);
// Return early if the file is not valid
if (!checkFile(file)) {
return {};
}
// Try to extract the key from the file
auto key = extractKey(file.get());
return key;
}
Finally
The finally
function from Guidelines support library is another mechanism for managing the lifetime of resources and ensuring that they are properly cleaned up when a function returns. Similar to smart pointers with custom deleters, it allows us to execute a function or lambda expression when it goes out of scope. For example, we can update our getKeyFromFile
function to use gsl::finally
as follows:
Key getKeyFromFile(const char* file_name) {
// Open the file as FILE*
FILE* file = std::fopen(file_name, "r");
// Create finally object with lambda function that calls fclose function when goes out of scope
gsl::finally close_file([&] { std::fclose(file); });
// Return early if the file is not valid
if (!checkFile()) {
return {};
}
// Try to extract the key from the file
auto key = extractKey(file);
return key;
}
In this example, the lambda expression passed to finally
will be executed when close_file
goes out of scope, regardless of whether the function returns normally or an exception is thrown. This ensures that the file handle is closed and the resource is properly cleaned up.
Conclusion
In conclusion, RAII is a powerful technique for managing the lifetime of resources and ensuring that they are properly cleaned up when a function returns. As suggested by the C++ Core Guidelines, it is generally best to use RAII to prevent resource leaks whenever possible. Both smart pointers with custom deleters and the gsl::finally
function provide an easier way that doesn't require another encapsulation layer. You can use the RAII technique to ensure that your code is free from resource leaks.