Skip to Content
Course content

102: RAII for Exception Safety

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

I want to show you a snippet of code that looks perfectly reasonable at a first glance. It's the kind of logic I see in a lot of mid-level code reviews—code that works 99% of the time, but crashes or leaks resources in the one scenario that actually matters: when things go wrong.

void processSystemLogs(const std::string& path) {
    // Allocate a buffer for log data
    char* buffer = new char[4096]; 
    
    if (!loadFileIntoBuffer(path, buffer)) {
        delete[] buffer;
        return;
    }

    // This function analyzes the data and might throw a 
    // std::runtime_error if the log format is corrupted.
    analyzeLogData(buffer); 

    delete[] buffer;
    std::cout << "Log processed successfully." << std::endl;
}

The Leak You Won't See in the Debugger

If you run this with a healthy log file, it's a non-event. The buffer is allocated, analyzed, and deleted. But look at analyzeLogData(buffer). I've noted that it can throw an exception. In C++, when an exception is thrown, the runtime starts "stack unwinding." It jumps straight out of the current function to find the nearest catch block.

Here is the problem: the delete[] buffer line is skipped entirely. Because buffer is a raw pointer, it's just a number on the stack. When the function exits prematurely due to the exception, that number vanishes, but the 4KB of heap memory it pointed to stays allocated. If this is a long-running server process that processes thousands of logs, you've just built a slow-motion memory leak that will eventually trigger an Out-Of-Memory (OOM) kill.

You might be tempted to wrap the whole thing in a try-catch block and call delete[] in the catch. Don't do that. It's tedious, it clutters your logic, and as your functions grow, you'll inevitably forget one exit path.

Tying Resource Lifetime to Scope

The professional way to handle this is RAII: Resource Acquisition Is Initialization. Despite the clunky name, the concept is simple: wrap your raw resource inside an object. Since C++ guarantees that a local object's destructor is called when it goes out of scope—regardless of whether the scope ended via a return or an exception—the destructor becomes your safety net.

In modern C++, we rarely write our own wrappers for memory because std::unique_ptr does it for us. Here is how I would rewrite that function to be exception-safe:

#include <memory>

void processSystemLogs(const std::string& path) {
    // Use a smart pointer instead of a raw pointer
    auto buffer = std::make_unique<char[]>(4096); 
    
    // We use buffer.get() to pass the raw pointer to the legacy C-style function
    if (!loadFileIntoBuffer(path, buffer.get())) {
        return; // No manual delete needed!
    }

    analyzeLogData(buffer.get()); 

    // No manual delete needed!
    std::cout << "Log processed successfully." << std::endl;
}

Now, if analyzeLogData throws an exception, the std::unique_ptr object is destroyed as the stack unwinds. Its destructor automatically calls delete[]. The memory is reclaimed instantly, and your application stays lean. I've spent way too many nights hunting down memory leaks that were caused by "forgotten" deletes in error paths; using RAII effectively eliminates that entire category of bugs.

Beyond Just Memory

RAII isn't just for new and delete. It's a pattern for anything that has a "start" and an "end." Think about mutexes: you lock them, you do work, you unlock them. If you forget to unlock a mutex because an exception was thrown, you've just created a deadlock that will freeze your entire program. This is why we use std::lock_guard or std::unique_lock. They lock the mutex in the constructor and unlock it in the destructor. The pattern is always the same: move the cleanup logic out of the main function body and into a destructor.




📋 Practical Task

Implementing a RAII-based File Descriptor Wrapper

In low-level C++ programming, you'll often deal with OS-level resources like file descriptors (ints in Linux/Unix) that must be closed using close(). If an exception occurs between open() and close(), the file descriptor leaks.

Your Task: Create a class called FileDescriptor that manages a file descriptor using RAII.

  • The constructor should take a const char* filename and a int flags argument, calling open() to acquire the descriptor.
  • The class should store the descriptor as a private member.
  • The destructor must call close() on the descriptor if it is valid (i.e., not -1).
  • Implement a method int get() const that returns the raw descriptor so it can be used with other system calls.
  • To prevent double-closing, disable the copy constructor and copy assignment operator (use = delete).

Test your class by creating a function that instantiates FileDescriptor and then intentionally throws a std::runtime_error. Use a tool like lsof or simply observe that the file handle is released properly.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.