5.2.29 · D5C++ Programming
Question bank — Exception safety — basic, strong, no-throw guarantees
True or false — justify
Each statement is either true or false. Say which, and why — the reason is the point.
A noexcept function automatically provides the strong guarantee.
True — if it can never throw, then "the operation has no effect on a throw" is vacuously satisfied because the throw can never happen. No-throw sits above strong in the ladder.
The strong guarantee means the operation cannot fail.
False — it can absolutely fail (throw); the promise is only that on failure the state is rolled back to exactly what it was before. "Cannot fail" is the separate no-throw guarantee.
The basic guarantee promises that variable values stay unchanged after a throw.
False — basic only promises no leaks and a valid, destructible but unspecified state. Values may have changed; only invariants are guaranteed to still hold.
If every resource is wrapped in RAII, you automatically get the strong guarantee.
False — RAII buys you the basic guarantee (no leaks, cleanup on unwind). Strong requires an extra commit-or-rollback design like copy-and-swap on top of RAII.
A function that throws before modifying any observable state provides the strong guarantee.
True — if the throw happens before any visible change, the caller sees exactly the pre-call state, which is precisely "no effect". Order-of-operations can hand you strong for free.
swap being noexcept is what lets copy-and-swap commit atomically.
True — the risky copy work happens on a temporary; the final
swap is the only step touching the real object, and because it can't throw, the commit either fully happens or is never reached mid-way.Marking a function noexcept makes it faster and safer with no downside.
False — if a
noexcept function ever throws, the runtime calls std::terminate with no stack unwinding. Lying about no-throw is fatal, not free.The "no guarantee" level still guarantees no undefined behaviour.
False — "no guarantee" is the worst case: leaks, broken invariants, double-frees, and undefined behaviour are all possible. It is the state we design against.
A destructor that throws is fine as long as you catch the exception inside it.
Roughly true only if it never lets the throw escape — a destructor that lets an exception leave during stack unwinding causes
std::terminate. Practically, treat destructors as effectively noexcept.std::vector::push_back always gives the strong guarantee.
True for the
push_back call in isolation (on reallocation it rolls back), but only because reallocation copies or noexcept-moves. Composed with other side-effecting statements, the enclosing function may drop to basic.Spot the error
Each snippet claims a guarantee. Say what's wrong, or confirm it's right and why.
void g(){ auto p = new Widget(); p->risky(); delete p; } — claims basic.
Wrong — if
p->risky() throws, delete p is skipped and the Widget leaks. Raw new/delete around a throwing call gives no guarantee; fix with unique_ptr.Buffer& operator=(const Buffer& rhs){ delete[] data; data = new int[rhs.n]; ... } — claims strong.
Wrong on two counts:
new int[...] can throw after delete[] data, leaving data dangling (broken invariant), and it isn't self-assignment safe. Pass-by-value copy-and-swap fixes both.Copy-and-swap where swap is a plain member-by-member std::swap on int and pointers — claims strong.
Correct — swapping built-in scalars and raw pointers is
noexcept, so the commit can't throw and the throwing copy work stayed on the temporary. This is the intended shape.void h(std::vector<int>& v,int x){ v.push_back(x); log(x); } where log may throw — claims strong.
Wrong — after
push_back succeeds, a throw in log leaves the element already pushed. The visible side effect persists, so this is only basic. Do the push on a copy and swap last for strong.A move constructor Buffer(Buffer&& o) noexcept { data=o.data; o.data=nullptr; } marked noexcept but it calls a throwing logger inside.
Wrong — the body can throw via the logger, so the
noexcept is a lie; a real throw here calls std::terminate. Either remove the throwing call or drop noexcept.friend void swap(Buffer&a,Buffer&b){ Buffer t=a; a=b; b=t; } used inside copy-and-swap.
Wrong — this "swap" copies (calls copy constructor/assignment, which can throw), defeating the whole point. Real swap must exchange handles member-wise and be
noexcept.Why questions
Answer the "why" in one or two sentences of real reasoning.
Why does RAII give the basic guarantee automatically?
Because each resource's release is tied to a stack object's destructor, and stack unwinding runs those destructors on any throw — so cleanup can never be "skipped over" like a missed
delete.Why must the swap step of copy-and-swap be the one that can't throw, rather than the copy?
The copy is allowed to throw because it only touches the temporary, leaving the original intact; the swap is where the observable object changes, so it's the only step that must be atomic and non-throwing.
Why does std::vector sometimes copy instead of move when reallocating?
A move that throws midway would leave the old buffer partly gutted, destroying the strong guarantee. If the element's move constructor isn't
noexcept, the vector copies (source untouched) so it can still roll back — the std::move_if_noexcept rule.Why is a composed operation only as strong as its weakest rollback?
Because guarantees describe the whole function's after-throw state; a later throwing step that already committed an earlier visible change means the whole cannot claim "no effect", even if each piece was individually strong.
Why does a noexcept violation call std::terminate instead of just propagating?
The compiler used the
noexcept promise to skip generating unwinding machinery for callers; letting an exception escape would break that assumption, so the standard mandates immediate terminate.Why is copy-and-swap self-assignment safe "for free"?
In the pass-by-value form the argument is already an independent copy made before the body runs, so swapping with yourself just swaps the object with its own copy — no aliasing hazard, no special check needed.
Why do we say noexcept on move constructors is a performance feature, not just safety?
Because it lets containers like
std::vector move elements during reallocation instead of copying them; without the noexcept promise they fall back to slower copies to preserve strong safety.Edge cases
The boundary and degenerate cases the topic quietly invites.
What guarantee does a function that does absolutely nothing (empty body) provide?
The no-throw guarantee — it can't throw and has no observable effect, so it sits at the top of the ladder trivially.
Appending zero elements (k == 0) in the copy-and-swap append — what guarantee holds?
Still strong: even a no-op path copies then swaps (or short-circuits before any visible change), so the original is untouched either way and there's nothing to roll back.
If Buffer tmp(*this) throws with a huge allocation, what state is *this in?
Exactly its pre-call state — the throw happened before any swap, so nothing observable changed. This is the strong guarantee working as designed.
A type whose move constructor is not marked noexcept but genuinely never throws — is the vector optimal?
No — the vector can't see that it's safe, so it conservatively copies. The optimization is unlocked only by the visible
noexcept promise, not by the actual runtime behaviour.Self-assignment x = x through a naive "delete then copy" assignment — what breaks?
delete[] data frees the very buffer you're about to copy from, so you copy freed memory: undefined behaviour. Copy-and-swap avoids this because the copy is made before any deletion.What guarantee does allocation via new alone give if it throws before you store the pointer?
The strong guarantee for that step —
new that throws (std::bad_alloc) never returns a pointer, so nothing was acquired and nothing leaks; state is unchanged.If every operation in a chain is noexcept except a final logging call, what's the overall guarantee?
Only basic (or weaker) — the whole is capped by the throwing step, which may run after earlier visible changes have committed, so "no effect" can no longer be promised.
Recall One-line summary to lock it in
Ladder question ::: Basic = valid + no leak (RAII); Strong = commit-or-rollback (copy-and-swap on a temporary); No-throw = can't throw (noexcept swaps/moves/dtors). A composed operation is only as strong as its weakest rollback.