Exception safety in C++ is a promise about what a function leaves behind if something throws partway through. The three guarantees are basic, strong and nothrow. A function marked [[noexcept]] promises that it will never let an exception escape, and marking it wrongly is a serious bug because the compiler is allowed to assume the promise holds.
The three guarantees
Basic guarantee — if an exception is thrown, the object is left in a valid but unspecified state. Nothing is corrupted, but the operation did not complete and the value may have changed.
Strong guarantee — if an exception is thrown, the operation has no effect at all. This is the transaction guarantee, and it is the one you want for anything that updates state.
Nothrow guarantee — the function never throws.
#include
#include
#include
class Ledger {
public:
void add_entry(const std::string& entry) {
entries_.push_back(entry); // basic guarantee
}
void replace_all(const std::string& entry) {
std::vector copy = entries_; // strong guarantee
copy.push_back(entry);
entries_ = std::move(copy);
}
int count() const {
return static_cast(entries_.size());
}
private:
std::vector entries_;
};
int main() {
Ledger ledger;
ledger.add_entry("opening");
std::cout << "count: " << ledger.count() << '\n';
ledger.replace_all("adjustment");
std::cout << "count: " << ledger.count() << '\n';
return 0;
}The output shows the two operations working:
count: 1
count: 2The replace_all function is the strong guarantee in action: it builds the new state in a local variable and only commits it once everything has succeeded.
Copy and swap
Copy-and-swap is the standard technique for getting the strong guarantee, and it also removes the self-assignment problem.
#include
#include
#include
class Price {
public:
Price(int cents, const std::string& currency)
: cents_(cents), currency_(currency) {}
Price(const Price& other) = default;
Price& operator=(Price other) {
swap(other);
return *this;
}
void swap(Price& other) noexcept {
std::swap(cents_, other.cents_);
std::swap(currency_, other.currency_);
}
int cents() const { return cents_; }
private:
int cents_;
std::string currency_;
};
int main() {
Price first(100, "GBP");
Price second(250, "GBP");
first = second; // copy, then swap
std::cout << "first: " << first.cents() << '\n';
first = first; // self-assignment is safe
std::cout << "still: " << first.cents() << '\n';
return 0;
}The output shows both the assignment and the self-assignment working:
first: 250
still: 250The trick is that the parameter of the assignment operator is taken by value. The argument is copied into the parameter first — and if that copy throws, nothing has changed yet — and then the swap is a noexcept operation that cannot fail.
What noexcept means
Marking a function noexcept promises that no exception escapes it. It is not decorative: the compiler uses the promise to generate better code, and the standard library uses it to decide whether a container can move elements during a reallocation.
#include
#include
#include
void may_throw() {
throw std::runtime_error("something went wrong");
}
int main() {
try {
may_throw();
} catch (const std::exception& error) {
std::cout << "caught: " << error.what() << '\n';
}
std::cout << "noexcept(int) is " << std::boolalpha << noexcept(1 + 1) << '\n';
std::cout << "noexcept(may_throw) is " << noexcept(may_throw) << '\n';
return 0;
}The output shows the operator reporting whether an expression can throw:
caught: something went wrong
noexcept(int) is true
noexcept(may_throw) is trueNote that may_throw itself is not marked noexcept, so throwing from it is fine. The promise is about the function, not about the code inside it.
When noexcept is right
- Destructors. A destructor is implicitly
noexcept. If it throws during stack unwinding, the program terminates. - Move constructors and move assignments. Marking them
noexceptlets a vector move rather than copy when it grows. - Swap functions. A swap that cannot throw makes copy-and-swap give the strong guarantee.
- Functions that genuinely cannot throw. A getter that only returns a member is a good candidate.
Never mark a function noexcept unless you are certain. If it does throw, the program calls std::terminate and dies immediately, which is much harder to debug than an exception.
#include
#include
void dangerous() noexcept {
throw std::runtime_error("this terminates the program");
}
int main() {
std::cout << "before\n";
try {
dangerous();
} catch (...) {
std::cout << "never printed\n";
}
return 0;
}This program prints "before" and then terminates. The catch block never runs, because a noexcept function that throws calls std::terminate rather than propagating.
The basic guarantee in practice
A function that updates several members one at a time gives only the basic guarantee. If the second update throws, the first has already happened.
#include
#include
#include
class Settings {
public:
void apply(const std::string& name, int value) {
name_ = name; // if this throws, nothing changed
value_ = value; // if this throws, the name changed
}
void apply_safely(const std::string& name, int value) {
Settings copy = *this; // work on a copy
copy.name_ = name;
copy.value_ = value;
*this = std::move(copy); // commit only if everything worked
}
const std::string& name() const { return name_; }
int value() const { return value_; }
private:
std::string name_;
int value_ = 0;
};
int main() {
Settings settings;
settings.apply_safely("threshold", 42);
std::cout << settings.name() << '=' << settings.value() << '\n';
return 0;
}The output shows the safe version committing the change:
threshold=42Quick reference
- Basic guarantee: valid but unspecified state after a throw.
- Strong guarantee: no effect at all after a throw — the transaction guarantee.
- Nothrow: the function never throws.
- Copy-and-swap gives the strong guarantee and handles self-assignment.
- Mark move operations and
swapnoexceptso containers can move rather than copy. - Never mark a function
noexceptunless it truly cannot throw.
Try it yourself: write a class that updates two members in one method and throws partway through, and observe that the first update survives. Then rewrite it using copy-and-swap and check that the throw leaves the object untouched. Finally, mark a function [[noexcept]] that throws and watch the program terminate instead of unwinding.