Exception Safety
The basic idea is that we want our program to be exception safe. Introduced in CS247.
There are 4 levels of exception safety:
- No exception safety - If an exception is thrown - no guarantees about program state - object has invalid memory, memory is leaked, program crashes.
- Basic guarantee - if an exception is thrown - program is in a valid, but unspecified state. Ex: Class invariants are maintained, no memory is leaked.
- Strong guarantee - If an exception is thrown, the program reverts back to its state before the method was called. Ex:
vector::emplace_back
- Either it succeeds, or if exception is thrown, vector is in its prior state
- Nothrow guarantee - exceptions are never propagated outside of the function call - always does its job. Ex:
vector::size
givesnothrow
guarantee, always returns size of vector without fail.
Motivation
Consider the following program:
Under normal circumstances, f
does not leak memory. But, if g
throws, then we do not execute delete p
, so we do leak memory!
Thing to recognize
Exceptions significantly change control flow! We no longer have the guarantee of sequential execution.
A fix is to use unique_ptr.
Why do we care if the program is going to crash anyways?
We care about memory leaks even when a program is going to crash due to factors like:
- Resource Management: The operating system might not reclaim all resources, affecting other programs’ performance.
- Debugging and Maintenance: Memory leaks can be indicative of other code problems.
- Reliability and Robustness: Repeated crashes and memory leaks in a larger system can cause system-wide issues.
- Graceful Failure: Even in a crash, proper resource cleanup provides better debugging information and minimizes impact on the system.
To avoid such leaks, use exception-safe practices like Smart Pointers.
Exercise
What is the exception safety of f
, given that A::g
and B::h
both provide the strong guarantee?
- Answer:
f
- only satisfies the basic guarantee.
Why?
This is because if
a.g()
throws, then because it provides strong guarantee - it’s as if it was never called. When exception propagates fromf
,f
has not done anything.If
b.h()
throws - it provides the strong guarantee, so it does undo any of its effects. BUT: it has no knowledge thata.g()
ran just prior. As a result, when exception propagates fromf
, the effects ofa.g()
remain in place.Program is in a valid yet unspecified state basic guarantee.
Right now, there may be effects of a.g()
that are impossible to undo writing to a file, printing to the screen, sending a network request.
For simplicity, we’ll assume A::g
and B::h
have only local side effects (i.e. only the a
and b
objects and any associated memory may change).
Now, how may we try to rewrite f
to achieve the strong guarantee?
Idea: Use Copy-and-Swap Idiom, only work on temporary objects rather than real ones.
If aTemp.g()
or bTemp.h()
throw - no changes are made to the C
object, it’s as if f
was never called.
This is unfortunately, still the basic guarantee. Because, if b = bTemp
throws, we propagate an exception having modified a
.
What we really need for this is a non-throwing swap or assignment. Assigning a pointer will never throw. One possible solution: PImpl Idiom.
This is the strong guarantee - if a.g()
or b.h()
throw - all work is done on temp
, so we’re fine.
Otherwise, swap will always succeed f
will do its job.
vector::emplace_back
. Gives us the strong guarantee. How?
Easy case: No resize, just put the object in the array. When resizing:
- allocate a new, larger array
- Invoke array copy constructor to copy objects of type
T
to new array- If copy constructor throws: delete the new array, old array is left intact.
- Then delete old array, and set array pointer to new array (nothrow).
Complaint: This is a lot of copies when all I really want was to resize my array.
Better, would be to move:
- Allocate a new array
- Move objects from the old array to the new array
- If a move throws, move all our objects back from new array to old array, delete new array
- Delete old array, set pointer to new array
Problem: If move throws once, it might also when moving objects back. Once we’ve modified our old array, no guarantee we can restore it.
Solution: If move constructor is declared as “noexcept”, then emplace_back
will perform moves. Otherwise, it will copy over every item, and do this try-catch block to delete old items if necessary.
If possible, moves and swaps should provide the nothrow guarantee - and you should make this explicit to the compiler via the noexcept tag.
If you know a function will never propagate an exception - declare it noexcept to facilitate optimization.
Moves and swaps at the minimum should be noexcept.
Nothrow Guarantee
Does adding the
noexcept
keyword automatically mean nothrow guarantee?I thought yes, but no. It does not.
No, the
noexcept
keyword in C++ does not automatically mean a “nothrow” guarantee. It is a promise that a function will not emit exceptions, but if an exception is thrown inside anoexcept
function, it will callstd::terminate
.So nothrow guarantees are much stronger than
noexcept
, becausenoexcept
can still throw.
Examples of nothrow guarantees:
std::swap
vector::size