Exception safety
An exception can leave a function at any point, so every operation that may throw has to say what state it leaves behind. The basic guarantee is that the object is still valid; the strong guarantee is that the failed operation changed nothing at all.
The standard containers give the strong guarantee for most operations, and a class written with copy and swap gets it for free. This lesson shows both, then an object that is half-changed and a resource that leaks because the throw came before the delete.
The two guarantees are a spectrum, and most functions give the basic one because it is cheap. The strong one is worth the copy when the operation is all-or-nothing for the caller, such as an assignment or an insert that must not half-happen.
The strong guarantee
push_back on a vector either appends the element or leaves the vector unchanged. The program tries to append a string of a trillion characters, which the library refuses with std::length_error before anything is added.
After the exception the vector still has two elements, and the message says why the operation failed. That is the strong guarantee: an operation that throws has no effect.
The guarantee costs the library some work: it checks the size first, allocates the new buffer, and only then moves the elements. A class that wants the same property uses the next section's pattern.
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
int main()
{
std::vector<std::string> values{"a", "b"};
try {
values.push_back(std::string(1000000000000, 'x'));
} catch (const std::exception& error) {
std::cout << "failed: " << error.what() << '\n';
}
std::cout << values.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra strong.cpp -o strong
./strong
failed: std::bad_alloc
2
An object half-changed
The account below subtracts the amount first and throws afterwards. The balance is already lower when the exception leaves, and the counter that should match it was never incremented.
The program prints a balance of -50 and zero withdrawals: the object is still valid, which is the basic guarantee, but it no longer means what it says. The invariant of lesson 024 is broken.
The fix is to check before changing anything, which gives the strong guarantee, or to do the change on a copy and swap it in only when the whole operation succeeded.
#include <iostream>
#include <stdexcept>
class account
{
public:
void withdraw(int amount)
{
balance_ -= amount;
if (amount > 100) {
throw std::runtime_error{"amount too large"};
}
++withdrawals_;
}
int balance() const
{
return balance_;
}
int withdrawals() const
{
return withdrawals_;
}
private:
int balance_ = 100;
int withdrawals_ = 0;
};
int main()
{
account a;
try {
a.withdraw(150);
} catch (const std::exception&) {
}
std::cout << a.balance() << ' ' << a.withdrawals() << '\n';
}
g++ -std=c++23 -Wall -Wextra basic.cpp -o basic
./basic
-50 0
Copy and swap
The pattern is to copy the new state into a temporary, modify the copy, and swap the temporary into the object only when every step succeeded. The object is unchanged if the copy or the modification throws.
The program's assign compares a threshold and throws before the swap when the new balance is too low. The printed balance is the original one, and the withdrawals counter still matches it.
swap itself must not throw, which is why it is marked noexcept. The standard library assumes that for the containers, and a user type should follow the same rule.
#include <iostream>
#include <stdexcept>
#include <utility>
class account
{
public:
account(int balance, int withdrawals) : balance_(balance), withdrawals_(withdrawals) {}
void assign(const account& other)
{
account copy{other};
if (copy.balance_ < 0) {
throw std::runtime_error{"negative balance"};
}
swap(copy);
}
void swap(account& other) noexcept
{
std::swap(balance_, other.balance_);
std::swap(withdrawals_, other.withdrawals_);
}
int balance() const
{
return balance_;
}
int withdrawals() const
{
return withdrawals_;
}
private:
int balance_;
int withdrawals_;
};
int main()
{
account original{100, 1};
account bad{-5, 9};
try {
original.assign(bad);
} catch (const std::exception& error) {
std::cout << error.what() << '\n';
}
std::cout << original.balance() << ' ' << original.withdrawals() << '\n';
}
g++ -std=c++23 -Wall -Wextra copy_swap.cpp -o copy_swap
./copy_swap
negative balance
100 1
swap must not throw
The copy-and-swap pattern relies on a swap that cannot fail: if the swap threw after the copy was ready, the object would be in a half-swapped state. Marking the function noexcept makes the compiler enforce that.
The program asks the type traits whether the account can be moved and swapped without throwing, and both answers are 1. The static_assert turns the property into a build error if a later change breaks it.
The same check is what lesson 080 uses to decide whether a vector may move elements during a reallocation.
#include <iostream>
#include <type_traits>
#include <utility>
class account
{
public:
account(int balance) : balance_(balance) {}
account(const account&) = default;
account& operator=(const account&) = default;
account(account&&) noexcept = default;
account& operator=(account&&) noexcept = default;
private:
int balance_;
};
int main()
{
static_assert(std::is_nothrow_move_constructible_v<account>);
static_assert(std::is_nothrow_swappable_v<account>);
std::cout << std::is_nothrow_move_constructible_v<account> << ' '
<< std::is_nothrow_swappable_v<account> << '\n';
}
g++ -std=c++23 -Wall -Wextra nothrow.cpp -o nothrow
./nothrow
1 1
The resource that leaks
A raw new followed by a throw leaks, because the delete is after the throw and never runs. The object's destructor would run, but a pointer is not an object with a destructor.
The program counts destructors and prints 0: the resource was created and never destroyed. The exception unwound the stack, which destroys objects, not pointers.
The fix is an object that owns the resource, such as the wrapper of lesson 027 or a std::unique_ptr from part 8. Copy and swap and RAII are the two tools that make exception safety routine.
#include <iostream>
#include <stdexcept>
int destroyed = 0;
struct resource
{
~resource()
{
++destroyed;
}
};
void work()
{
resource* handle = new resource;
throw std::runtime_error{"failed"};
delete handle;
}
int main()
{
try {
work();
} catch (const std::exception&) {
}
std::cout << destroyed << '\n';
}
g++ -std=c++23 -Wall -Wextra leak.cpp -o leak
./leak
0
Exercises
- Move the check in basic.cpp before the subtraction and confirm the strong guarantee in the output.
- Add a third member to account and its swap call, and check that noexcept still holds.
- Replace the raw pointer in leak.cpp with an object that owns it and check that destroyed becomes 1.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the basic guarantee?
Answer
The object stays valid, though its value may have changed
Every operation that may throw leaves a usable object behind.
-
What is the strong guarantee?
Answer
A failed operation changed nothing
The operation either succeeds completely or has no effect.
-
What does copy and swap give?
Answer
The strong guarantee for an assignment
The work happens on a copy, and the swap cannot throw, so the target changes only at the end.
-
Why must the swap in copy and swap be noexcept?
Answer
A throwing swap in the middle would leave both objects broken
The final step has to be a no-fail operation for the guarantee to hold.
-
What does this program print?
#include <iostream> #include <new> int main() { try { throw std::bad_alloc{}; } catch (const std::exception& error) { std::cout << "failed: " << error.what() << '\n'; } std::cout << 2; }Answer
failed: std::bad_alloc / 2
The catch prints the exception's message, and the program continues to the final print.
-
What does this program print?
#include <iostream> struct account { int balance = 100; void withdraw(int amount) { balance -= amount; } }; int main() { account a; a.withdraw(150); std::cout << a.balance << ' ' << 0; }Answer
-50 0
The operation has no check, so the balance goes negative; the program is valid but the value is wrong.
-
What does this program print?
#include <iostream> #include <stdexcept> #include <utility> class buffer { public: explicit buffer(int size) : size_(size) {} void swap(buffer& other) noexcept { std::swap(size_, other.size_); } buffer& operator=(buffer other) { swap(other); return *this; } int size_; }; int main() { buffer first{100}; buffer second{1}; first = second; std::cout << first.size_ << ' ' << second.size_; }Answer
1 1
The assignment takes the source by value and swaps, so the target gets 1 and the source is unchanged.
-
What does this program print?
#include <iostream> #include <utility> struct item { int value; void swap(item& other) noexcept { std::swap(value, other.value); } item& operator=(item other) { swap(other); return *this; } }; int main() { item a{1}, b{2}; a = b; std::cout << (a.value == 2) << ' ' << (b.value == 2); }Answer
1 1
The target takes the source's value, and the source keeps its own because the copy was swapped with.
-
Which operation cannot be part of a strong guarantee without a swap?
Answer
An assignment that frees the old resource first
Freeing first destroys the old state before the new one is ready.
-
What does the basic guarantee allow after a failed operation?
Answer
A changed but valid object
The object can be used and destroyed, but its value may differ.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev