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
103: The Three Levels of Exception Safety Guarantees
I've seen this exact bug surface in production more times than I care to admit. It usually happens when a developer thinks, "I'll just clear the old state and load the new state," without considering what happens if the loading process fails halfway through. In C++, this is where exception safety guarantees become the difference between a recoverable error and a corrupted application state.
The Vanishing Settings Bug
Imagine you're writing a SettingsManager that holds a map of configuration values. You want a method to update these settings from a data source. Here is a common, but dangerous, way to implement that:
struct SettingsManager {
std::map<std::string, std::string> config;
void updateSettings(const std::map<std::string, std::string> नए_settings) {
// Step 1: Clear existing settings
config.clear();
// Step 2: Populate with new settings
for (const auto& [key, val] : नए_settings) {
// Imagine this throws a std::bad_alloc or a custom validation exception
config.insert({key, val});
}
}
};
At first glance, this looks fine. But look closer at the sequence. If config.insert throws an exception on the fifth element of a ten-element map, your SettingsManager is now in a "zombie" state. You've already cleared the old, working settings, and you only have half of the new settings. The object is still "valid" in the sense that it hasn't crashed the CPU, but your application's logic is now broken because the configuration is incomplete. This is a classic failure of exception safety.
Implementing the Strong Guarantee with Copy-and-Swap
To fix this, we need to move from a "Basic Guarantee" (where we don't leak memory but the state is unpredictable) to a "Strong Guarantee" (where the operation either succeeds completely or leaves the state exactly as it was). The industry standard for this is the Copy-and-Swap idiom.
Instead of mutating the live state immediately, we perform the "risky" work on a temporary copy. If the copy process throws, the original state remains untouched. Only when we know the new state is fully constructed do we swap it into place using a non-throwing operation.
struct SettingsManager {
std::map<std::string, std::string> config;
void updateSettings(const std::map<std::string, std::string> new_settings) {
// Create a temporary copy of the current state (or start fresh)
// If this throws, 'this->config' is still safe and sound.
std::map<std::string, std::string> temp_config = new_settings;
// Perform any additional validation or modification on temp_config here...
// If an exception happens here, we still haven't touched the real config.
// Finally, swap the temporary with the real state.
// std::swap for maps is guaranteed not to throw.
std::swap(this->config, temp_config);
}
};
By delaying the mutation until the very last moment, we've ensured that the SettingsManager is never left in a half-baked state. This is the essence of the Strong Guarantee.
Navigating Basic, Strong, and No-throw Guarantees
When you're designing a class, you don't always need the Strong Guarantee—it can be expensive because of the extra copying. You just need to know which level you are promising the user of your code.
- The Basic Guarantee: This is the bare minimum. If an exception is thrown, no resources (memory, file handles) are leaked, and all objects are still in a valid state. You can still call member functions on the object, but the data might not be what you expect. Most C++ Standard Library containers provide this by default.
- The Strong Guarantee: This is "commit-or-rollback" semantics. If the operation fails, the state of the program is exactly as it was before the call. This is what we achieved with Copy-and-Swap. It's highly desirable for critical state changes.
- The No-throw (Nothrow) Guarantee: The function promises it will never throw an exception. This is critical for destructors, move constructors, and swap functions. If you're writing a move constructor, mark it
noexcept; otherwise,std::vectormight refuse to use it during a resize to avoid breaking the Strong Guarantee.
My rule of thumb: Start with the Basic Guarantee to prevent leaks, upgrade to the Strong Guarantee for public-facing API methods that change state, and use noexcept for the low-level plumbing that makes the Strong Guarantee possible.
📋 Practical Task
Exercise: Hardening the TransactionJournal
You are maintaining a TransactionJournal class that keeps a history of financial transactions. The current implementation of appendBatch is dangerous: it reserves space and then pushes elements into a vector. If a push fails, the vector may have been resized, but the batch is only partially added, leaving the journal in an inconsistent state.
Your Task: Rewrite the appendBatch method to provide the Strong Exception Guarantee. Ensure that if any part of the batch addition fails, the journal remains exactly as it was before the function was called.
#include <vector>
#include <string>
#include <stdexcept>
#include <iostream>
struct Transaction {
int id;
double amount;
};
class TransactionJournal {
std::vector<Transaction> history;
public:
// This is the BROKEN method. Fix it!
void appendBatch(const std::vector<Transaction>& batch) {
// CURRENT BROKEN LOGIC:
// for (const auto& tx : batch) {
// history.push_back(tx); // If this throws, history is partially updated.
// }
// TODO: Implement Strong Guarantee here
}
size_t size() const { return history.size(); }
};
// Test harness
int main() {
TransactionJournal journal;
std::vector<Transaction> good_batch = {{1, 10.0}, {2, 20.0}};
try {
journal.appendBatch(good_batch);
std::cout < "Journal size: " < journal.size() < " (Expected 2)" < std::endl;
} catch (...) {
std::cout < "Failed!" < std::endl;
}
return 0;
}There are no comments for now.