Brief Introduction To RAII

RAII, not RALL, not RAIL, not RALI, is the abbreviation of “Resource Acquisition Is Initialization”. It is a C++ programming technique, which means if your code follow fellow requirement, you are a RAII programmer:

  1. Encapsulate each resources into a class.
    • The constructor acquires the resource.
    • The destructor release the resource.
  2. The instance should has temporary lifetime (in anyway).

So what does it mean in practice? Well lets see.

Why do we need it

There are many reason we might need it, but just consider one case, when we are writing C, there’s a problem that we have to keep in mind every single moment: memory leak. How good is it if we have a mechanism that can help us to free our unneeded memory automatically? You might come up with the smart pointer in C++11, that’s too far for today.

How does it work

For every instance that has temporary lifetime in C++, it will be destroy in somewhere and sometime, more specifically, at the end of it lifetime. Because it is a instance of a class, at least one destructor will called. Why is it at least one, if it is a derived class, its parent’s destructor will be called as well. So to ensure it has a temporary lifetime, in other words, it need to be on stack.

So far, we know that at the end of the lifetime of a instance, its destructor function will be call automatically, why don’t we just release the resources here, so it will be released automatically when we don’t need it. OK, that’s the idea of RAII.

An example of using RAII

As I mentioned before, RAII is a C++ programming technique, it can prevent memory leak, but not just memory leak, let’s see a problem of fixing file not closed problem:

#include <iostream>
#include <fstream>

using namespace std;

class FileReader{
private:
    string _fileName;
    fstream _fileStream;
public:
    FileReader(string fileName) : _fileName(fileName){
        cout << "You create a new File Reader Reading file " << _fileName << endl;
        _fileStream.open(_fileName, ios::in);
    };
    int read(){
        string line;
        getline(_fileStream, line);
        cout << line << endl;
        if(_fileStream.eof())
        {
            return 0;
        }
        return 1;
    }
    ~FileReader(){
        cout << "You don't need this File Reader Anymore" << endl;
        _fileStream.close();
    };
};

void doTest(){
    FileReader a("../main.cpp");
    while (a.read()){};
}

int main() {
    doTest();
    printf("End of do test\n");
    return 0;
}

In this test, instance a’s lifetime is within the doTest function, so once doTest is finished, a will be destroy in the meantime, and we don’t need to worry about closing the file or opening the file in the whole process.

What else can we improve with RAII

C++ already helped us to implement RAII to std::string, std::vector, std::jthread (since C++20) and smart pointers, but we still need to pay attention to open()/close(), lock()/unlock(), or init()/copyFrom()/destroy() and also member variables that require memory from heap. Finally, let’s see an example from cppreference, the lockguard here can be constructed as above similarly:

std::mutex m;
 
void bad() 
{
    m.lock();                    // acquire the mutex
    f();                         // if f() throws an exception, the mutex is never released
    if(!everything_ok()) return; // early return, the mutex is never released
    m.unlock();                  // if bad() reaches this statement, the mutex is released
}
 
void good()
{
    std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
    f();                               // if f() throws an exception, the mutex is released
    if(!everything_ok()) return;       // early return, the mutex is released
}