RAII: Resource Management in C++

RAII: Resource Management in C++

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

RAII is not the best name

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:

  1. If checkFile returns false, we close the file and return an empty key.

  2. If extractKey throws an exception, we close the file and rethrow the exception.

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

Raw pointers are scary

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 a unique_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 multiple shared_ptr instances can point to the same object, and the object will be deleted only when the last shared_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 a std::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 last shared_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

No more leaks

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.