Exception Safety — noexcept, RAII Guarantees, Strong and Basic Guarantees, Exception-Safe Code
In this tutorial, you will learn about Exception Safety. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ exception safety guarantees define how functions behave under exceptions — the no-throw guarantee, strong guarantee (atomic commit or rollback), basic guarantee (no leaks, valid state), and no guarantee — each with increasing risk.
What You'll Learn
You will understand the four exception safety levels and apply them to your code, use RAII to provide automatic cleanup during unwinding, write strong-guarantee functions using the copy-and-swap idiom, apply noexcept for move operations and swap, detect and fix exception-unsafe code patterns, and design interfaces that document their exception safety.
Why It Matters
Exception safety separates professional C++ from hobbyist code. Without it, a single exception can leak memory, corrupt data, or leave objects in invalid states. C++'s Exception Handling mechanism (stack unwinding) is powerful but unforgiving — RAII is the only reliable way to manage resources during exceptions. The STL itself requires containers to meet specific safety levels.
Learning Path
graph LR
A["58: Modules"] --> B["59: Exception Safety"]
B --> C["60: RAII & Resource Management"]
C --> D["61: Design Patterns in C++"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
The Four Exception Safety Levels
From safest to least safe:
- No-throw guarantee: The function will never throw.
- Strong guarantee: If an exception occurs, the program state is unchanged (atomic commit).
- Basic guarantee: No resources are leaked and objects remain in a valid (but unspecified) state.
- No guarantee: Anything can happen (avoid writing code at this level).
#include <iostream>
#include <vector>
#include <stdexcept>
class Account {
std::string name_;
double balance_;
public:
Account(std::string name, double balance)
: name_(std::move(name)), balance_(balance) {}
// No-throw guarantee: never throws
double getBalance() const noexcept {
return balance_;
}
// Strong guarantee: either succeeds or balance is unchanged
void deposit(double amount) {
if (amount < 0) throw std::invalid_argument("negative deposit");
// No operations that could fail after this point (ideally)
balance_ += amount;
}
// Basic guarantee: valid state, but could be partial
void transferTo(Account& other, double amount) {
// If withdraw throws, we're fine (no-op)
// If deposit throws, withdraw has already happened!
// This has only basic guarantee (state is valid but ambiguous)
balance_ -= amount; // Could make balance negative
other.deposit(amount); // If this throws, both accounts are wrong
}
// Strong-guarantee version via copy-and-swap
static void transfer(Account& from, Account& to, double amount) {
Account tempFrom = from; // Copy
Account tempTo = to; // Copy
tempFrom.balance_ -= amount;
tempTo.balance_ += amount;
// Only now commit: if swap throws, nothing changes
std::swap(from, tempFrom);
std::swap(to, tempTo);
}
};
noexcept Specifier
noexcept declares that a function will not throw exceptions. The compiler can optimize more aggressively.
#include <iostream>
#include <type_traits>
#include <utility>
// noexcept declaration
void safeFunction() noexcept {
// If an exception propagates here, std::terminate is called
}
// Conditional noexcept
template <typename T>
void swap(T& a, T& b) noexcept(std::is_nothrow_swappable_v<T>) {
using std::swap;
swap(a, b);
}
// noexcept is part of the function type
void mayThrow();
void noThrow() noexcept;
int main() {
std::cout << "mayThrow is noexcept: "
<< noexcept(mayThrow()) << "\n"; // false
std::cout << "noThrow is noexcept: "
<< noexcept(noThrow()) << "\n"; // true
// noexcept operator: compile-time check
auto lambda1 = []() noexcept {};
auto lambda2 = []() {};
std::cout << "lambda1: " << noexcept(lambda1()) << "\n"; // true
std::cout << "lambda2: " << noexcept(lambda2()) << "\n"; // false
}
Copy-and-Swap Idiom
The copy-and-swap idiom provides the strong exception safety guarantee for assignment.
#include <iostream>
#include <cstring>
#include <algorithm>
class String {
char* data_;
size_t size_;
void swap(String& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
public:
String(const char* str) : size_(std::strlen(str)), data_(new char[size_ + 1]) {
std::strcpy(data_, str);
}
// Copy constructor
String(const String& other) : size_(other.size_), data_(new char[size_ + 1]) {
std::strcpy(data_, other.data_);
}
// Destructor
~String() { delete[] data_; }
// Copy-and-swap assignment (strong guarantee)
String& operator=(String other) noexcept { // 'other' is passed by value (copy)
swap(other); // No-throw swap
return *this; // Old data destroyed with 'other'
}
// Move constructor
String(String&& other) noexcept
: data_(std::exchange(other.data_, nullptr)), size_(other.size_) {}
const char* c_str() const { return data_; }
};
int main() {
String s1("hello");
String s2("world");
s1 = s2; // Copy-and-swap: strong guarantee
std::cout << s1.c_str() << "\n"; // world
s1 = String("temporary"); // Move assignment (via the same operator)
std::cout << s1.c_str() << "\n"; // temporary
}
RAII and Exception Safety
RAII is the foundation of exception safety. Destructors run during stack unwinding.
#include <iostream>
#include <fstream>
#include <stdexcept>
// RAII wrapper for FILE*
class File {
FILE* file_;
public:
File(const char* filename, const char* mode)
: file_(std::fopen(filename, mode)) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~File() {
if (file_) std::fclose(file_);
}
// Move-only (no copy)
File(File&& other) noexcept : file_(std::exchange(other.file_, nullptr)) {}
File& operator=(File&& other) noexcept {
if (this != &other) {
if (file_) std::fclose(file_);
file_ = std::exchange(other.file_, nullptr);
}
return *this;
}
void write(const std::string& text) {
if (std::fputs(text.c_str(), file_) == EOF) {
throw std::runtime_error("Write failed");
}
}
};
void processFile() {
File f("test.txt", "w");
f.write("Hello, World!\n");
// If write throws, the File destructor runs and closes the file
// No resource leak!
}
int main() {
try {
processFile();
std::cout << "File written successfully\n";
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << "\n";
}
}
std::terminate and Unexpected Exceptions
When a noexcept function throws, std::terminate is called.
#include <iostream>
#include <exception>
void dangerous() noexcept {
throw std::runtime_error("This should not happen");
// noexcept is violated: std::terminate will be called
}
int main() {
std::set_terminate([]() {
std::cout << "Terminate called!\n";
std::abort();
});
// dangerous(); // Would call terminate
// noexcept in destructors
struct BadDestructor {
~BadDestructor() noexcept(false) {
throw std::runtime_error("destructor error");
}
};
// During stack unwinding, a second exception in destructor calls terminate
try {
BadDestructor b;
throw std::runtime_error("main exception");
} catch (...) {
// If b's destructor also throws: terminate
std::cout << "Caught exception\n";
}
}
Exception Safety in the STL
STL containers provide specific safety guarantees.
#include <iostream>
#include <vector>
#include <list>
int main() {
// std::vector: strong guarantee for push_back (unless move throws)
std::vector<int> v = {1, 2, 3};
try {
v.push_back(4); // If reallocation fails, vector is unchanged
} catch (...) {
std::cout << "push_back failed, size: " << v.size() << "\n"; // 3
}
// std::list: strong guarantee for all operations (no reallocation)
std::list<int> lst = {1, 2, 3};
try {
lst.push_back(4); // Always succeeds (no reallocation needed)
} catch (...) {
// Only fails if memory allocation fails
}
// emplace_back: strong guarantee
struct ThrowsOnCopy {
ThrowsOnCopy() = default;
ThrowsOnCopy(const ThrowsOnCopy&) { throw std::runtime_error("copy"); }
};
std::vector<ThrowsOnCopy> tricky;
tricky.emplace_back(); // Construct in place — no copy needed
// tricky.push_back(ThrowsOnCopy{}); // Would throw during copy
}
Common Mistakes
Mistake 1: Throwing from a destructor
struct Bad {
~Bad() {
throw std::runtime_error("error"); // Never do this!
}
};
If a destructor throws during stack unwinding (another exception active), std::terminate is called. Destructors should always be noexcept.
Mistake 2: Assuming strong guarantee without copy-and-swap
void update(Data& d) {
d.field1 = compute1(); // If compute2 throws, field1 is already modified
d.field2 = compute2(); // State is partially updated!
}
Use copy-and-swap for atomic updates.
Mistake 3: Not marking move operations noexcept
struct Movable {
Movable(Movable&&) { /* may throw */ } // No noexcept
};
// std::vector will copy instead of move during reallocation
STL containers prefer copy over move if move is not noexcept (for strong guarantee).
Mistake 4: Resource leak before RAII
void bad() {
int* p = new int(5);
func(); // If func throws, p leaks
delete p;
}
Use std::unique_ptr or other RAII wrappers.
Mistake 5: Swallowing all exceptions
try {
risky();
} catch (...) {
// Silently ignores all errors
}
Practice Questions
What are the four exception safety levels? Answer: No-throw, strong (atomic), basic (no leak, valid state), and no guarantee.
Why must destructors be noexcept? Answer: If a destructor throws during stack unwinding (when another exception is active), std::terminate is called.
What is the copy-and-swap idiom? Answer: The assignment operator takes the parameter by value (copy), then swaps with the copy. If the copy throws, *this is unchanged (strong guarantee).
What does
noexceptdo to function behavior? Answer: It declares the function will not throw. If an exception attempts to propagate, std::terminate is called.Why does std::vector require noexcept move constructors? Answer: For the strong exception safety guarantee during reallocation. If move throws, vector falls back to copy.
FAQ
Mini Project
Implement a ScopedTransaction class that provides the strong exception safety guarantee for a series of database-style operations:
#include <iostream>
#include <vector>
#include <string>
#include <functional>
// Your ScopedTransaction class
int main() {
std::vector<std::string> database;
ScopedTransaction transaction(database);
transaction.addOperation([&]() { database.push_back("Alice"); });
transaction.addOperation([&]() { database.push_back("Bob"); });
transaction.addOperation([&]() {
database.push_back("Charlie");
throw std::runtime_error("Simulated failure");
});
try {
transaction.commit();
} catch (...) {
std::cout << "Transaction failed, rolled back\n";
}
std::cout << "Database size after rollback: " << database.size() << "\n";
// Should be 0 if rollback worked correctly
}
This project demonstrates how C++ exception safety guarantees apply to real-world transactional systems, similar to database Transaction in Java JDBC or Python's context managers.
What's Next
You now understand exception safety — the discipline that separates robust C++ from fragile code. Next, you will deepen your knowledge of RAII and resource management, the pattern that makes exception safety possible.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro