C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
102: RAII for Exception Safety
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* filenameand aint flagsargument, callingopen()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() constthat 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.
There are no comments for now.