Exception Safety and the Strong Guarantee
Exception safety is the contract a function makes about program state when an exception passes
through it. The four levels form a ladder; most code should aim for the strong guarantee โ
all-or-nothing โ and noexcept (the no-throw rung) is the tool that makes the commit step possible.
This page is about the safety guarantees. For the noexcept specifier itself โ syntax, the
noexcept operator, conditional noexcept, which functions to mark, performance โ see the canonical
noexcept Specifier page.
The four levelsโ
| Guarantee | Promise on exception | Typical source |
|---|---|---|
| No guarantee | anything โ leaks, corruption | raw new/delete, no cleanup |
| Basic | no leaks, object still valid (but maybe changed) | partial update is acceptable |
| Strong | state is exactly as before โ operation rolled back | commit-or-rollback designs |
| No-throw | never throws at all | noexcept functions, swaps, dtors |
The diagram is the strong guarantee in one picture: do the throwing work on a copy, then commit with an operation that cannot throw. If the work throws, the original is untouched.
Basic vs strong, concretelyโ
// BASIC guarantee โ if push_back throws midway, some items are already in,
// count_ may disagree with data_. No leak, but state changed.
void addItems(const std::vector<int>& items) {
for (int x : items) { data_.push_back(x); ++count_; }
}
// STRONG guarantee โ all work happens on a copy; the only mutation of *this
// is the final noexcept move. Throw anywhere above and *this is unchanged.
void addItems(const std::vector<int>& items) {
std::vector<int> temp = data_; // copy (may throw)
temp.insert(temp.end(), items.begin(), items.end()); // (may throw)
data_ = std::move(temp); // commit (noexcept)
count_ += items.size();
}
This "prepare on a copy, commit with a non-throwing move/swap" shape is the core technique. The copy-and-swap idiom packages it for assignment operators; PIMPL does it by swapping a single pointer.
The whole scheme collapses if the commit can throw. That is why move assignment and swap need to be
noexcept โ and why the standard library only moves (instead of copying) elements during vector
reallocation when the move constructor is noexcept. A throwing move would break the strong
guarantee, so the library plays it safe and copies. See
noexcept and move semantics.
Rollback when you can't prepare on a copyโ
If copying the whole object is too expensive, achieve the strong guarantee by saving enough to undo:
void transactionalPush(int value) {
auto backup = data_; // save what we need to restore
try {
data_.push_back(value);
backup.clear(); // success โ drop the backup
} catch (...) {
data_ = std::move(backup); // rollback (noexcept move)
throw; // rethrow: caller sees the original state
}
}
Choosing a levelโ
You do not always want the strong guarantee โ it has a cost (the copy). Pick deliberately:
- No-throw for destructors, swaps, move operations, and simple observers โ and mark them
noexcept. - Strong for operations a caller will retry or that must not corrupt shared state (assignment, bulk updates, transactions).
- Basic when a copy is too expensive and the caller can cope with a valid-but-changed object.
Standard containers already provide strong/basic guarantees for their operations. The cheapest way
to be exception-safe is to build from std::vector, std::string, and smart pointers, and let
RAII handle every cleanup path โ then you rarely write a
try/catch at all.
Worked example โ an exception-safe Stackโ
Each method's guarantee is called out; note how the no-throw ones are noexcept and the mutating one
leans on vector's own strong guarantee.
template <class T>
class Stack {
std::vector<T> data_;
public:
void push(const T& v) { data_.push_back(v); } // strong (vector's)
void pop() noexcept { if (!data_.empty()) data_.pop_back(); }
bool empty() const noexcept { return data_.empty(); }
size_t size() const noexcept { return data_.size(); }
T top() const { // strong: returns by value
if (data_.empty()) throw std::out_of_range("empty stack");
return data_.back();
}
};
Summaryโ
- Exception safety has four levels: no-guarantee, basic, strong, no-throw.
- The strong guarantee = do throwing work on a copy, then commit with a no-throw move/swap.
- That commit step is why move/swap must be
noexceptโ and why containers copy instead of move when the move can throw. - Use rollback when a full copy is too costly; choose the level deliberately โ strong isn't free.
- Build on RAII and standard containers; they hand you most of these guarantees for free.
Relatedโ
- noexcept Specifier โ the specifier, operator, and where to apply it
- Copy-and-Swap ยท PIMPL ยท RAII
- Exceptions ยท Copy and Move Semantics