Skip to Content
Course content

50: RAII and Resource Management

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you something that separates the C++ developers who spend their weekends hunting for memory leaks from the ones who actually get to sleep. It's a concept called RAII—Resource Acquisition Is Initialization. I know, the name is absolutely terrible. It sounds like a textbook trying to be clever, but the logic behind it is the most powerful tool in the C++ arsenal.

The leak we didn't notice

Let's start with a scenario I've seen a dozen times. Suppose we're writing a simple function to log a message to a file. We'll use the C-style FILE* API just to keep the resource management explicit and "manual" for a moment.

void log_message(const char* msg) {
    FILE* file = fopen("log.txt", "a");
    if (!file) return;

    fprintf(file, "%s\n", msg);
    
    // Imagine some complex logic here...
    
    fclose(file);
}

On the surface, this looks fine. We open it, we write to it, we close it. But here is where the real world hits. Let's say I add a bit of validation logic, and if something is wrong, I return early.

void log_message(const char* msg) {
    FILE* file = fopen("log.txt", "a");
    if (!file) return;

    if (msg == nullptr) {
        return; // Oops! We just leaked the file handle.
    }

    fprintf(file, "%s\n", msg);
    fclose(file);
}

If msg is null, we exit the function, but fclose(file) never runs. The file handle stays open in the OS. If this function is called thousands of times in a long-running server, the OS will eventually tell us "Too many open files," and the whole application will crash. Now, you might say, "Just add another fclose before that return!" Sure, for one return. But what if you have five different error conditions? You'll be sprinkling cleanup code everywhere like confetti.

The fragility of manual cleanup

It gets worse when exceptions enter the picture. Let's say we're doing something that might throw an error.

void process_data() {
    FILE* file = fopen("data.bin", "rb");
    
    do_something_risky(); // What if this throws an exception?
    
    fclose(file); 
}

If do_something_risky() throws, the execution flow jumps straight out of the function to the nearest catch block. The line fclose(file) is skipped entirely. Now we have a leaked resource, and there is literally no way to put a "cleanup" call in the same block that handles the exception without duplicating code everywhere. This is the fundamental problem: manual resource management is fragile.

Tying the resource to the stack

Here is how I handle this. Instead of treating the resource (the FILE*) as a loose variable, I'm going to wrap it in a class. The key is to use the destructor. In C++, when an object on the stack goes out of scope, its destructor is guaranteed to run—no matter how the function exits (normal return, early return, or exception).

Let's build a simple guard for our file:

class FileGuard {
    FILE* handle;
public:
    FileGuard(const char* filename, const char* mode) {
        handle = fopen(filename, mode);
    }

    ~FileGuard() {
        if (handle) {
            fclose(handle);
            // I'll just put a comment here to visualize it working
            // std::cout << "File closed automatically!\n";
        }
    }

    FILE* get() { return handle; }
};

Now, look at how our log_message function changes. It becomes almost trivial:

void log_message(const char* msg) {
    FileGuard fg("log.txt", "a");
    if (!fg.get()) return;

    if (msg == nullptr) {
        return; // No leak! fg's destructor runs here.
    }

    fprintf(fg.get(), "%s\n", msg);
    // No manual fclose() needed here either.
}

I've effectively bound the "life" of the file handle to the "life" of the fg object. When fg is destroyed (which happens the moment we hit that return or if an exception is thrown), the destructor fires, and the file closes. That is RAII in a nutshell: Resource Acquisition Is Initialization. You acquire the resource in the constructor, and you release it in the destructor.

Why this actually works

You might be wondering why we don't just use a try...finally block like in Java or Python. C++ doesn't have finally because RAII is more powerful. It doesn't just work for files; it works for everything. Heap memory (via std::unique_ptr), mutex locks (via std::lock_guard), and socket connections all follow this pattern.

The beauty of this approach is that it moves the responsibility of cleanup from the user of the resource to the owner of the resource. As the person writing the FileGuard class, I've decided once and for all how the file should be closed. The person using the class doesn't even have to think about it. They just create the object and go about their business.




📋 Practical Task

Exercise: The Database Connection Guard

You are working on a system that interacts with a mock database. The database API provides two functions: db_connect() which returns a DBConn*, and db_disconnect(DBConn* conn) which closes the connection.

Currently, the code is leaking connections because developers are forgetting to call db_disconnect during error handling. Your task is to implement a RAII class called DbConnectionGuard.

  • The constructor should call db_connect() and store the resulting pointer.
  • The destructor must call db_disconnect() if the connection was successfully established.
  • Provide a method get_connection() to allow the user to access the underlying DBConn*.

Write your implementation and a small main()` function where you simulate an "early return" or "exception" to prove that the connection is closed regardless of how the scope is exited.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.