CS247 Lecture 22
Last Time: CRTP, Polymorphic cloning
This Time: Exception Safety, Template functions?
Recall: We saw that reasoning about programs with exceptions is tricky.
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 program’s performance.
- Debugging and Maintenance: Memory leaks can be indicative of other code problems.
- Reliability and Robustness: Repeated crashes and memory leaks in 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 in C++ (std::unique_ptr
, std::shared_ptr
), which manage memory automatically even when exceptions occur.
We can be more specific about how safe our program is given an exception is thrown.
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.
What is the exception safety of f
?
A::g
and B::g
both provide the string guarantee.
f
- only satisfies the basic guarantee
If a.g()
throws, then because it provides strong guarantee - it’s as if it was never called
- when exception propagates from
f
,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 that a.g()
ran just prior. As a result, when exception propagates from f
, the effects of a.g()
remain in place. ???????? (what does it mean effects of a.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, “pointer to implementation” idiom
Dereferencing pImpl
gives us a CImpl&
. Then we call make_unique<CImpl>
with this CImpl&
. make_unique
creates an entirely new unique pointer for us, and it will invoke the copy constructor for CImpl
(because make_unique
takes a CImpl
’s constructors arguments, and the constructor that takes in a const CImpl&
is the copy constructor).
Remember for unique_ptr
The copy ctor and copy assignment operator of unique_ptr is disabled by the standard library.
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.
Note
In this version of
C::f()
, you create a temporaryCImpl
object (temp
) and perform all modifications on it. This ensures that no changes are made to the actualC
object if an exception occurs. Thestd::swap
operation at the end is guaranteed not to throw exceptions, and it swaps the contents oftemp
andpImpl
, effectively committing the changes only if everything succeeds without exceptions.This code achieves the strong exception safety guarantee because if
a.g()
orb.h()
throws, theC
object remains unchanged, and if the swap operation throws, it’s done after the modifications are isolated in the temporary objecttemp
.
PImpl Idiom can also be used in some scenarios for reducing compilation dependencies.
typically:
C.h
On the other hand:
CImpl.h
C.h
Now, if cImpl
changes only C.cc
must recompile. Because we only use a forward declaration in C.h
no recompilation.
Explanation
With the PImpl idiom, you encapsulate the private implementation details in a separate structure (
CImpl
) that’s defined in its own header file (CImpl.h
). You only forward declareCImpl
inC.h
, which means you’re not exposing the actual implementation details to the code includingC.h
.
Finally - we could make CImpl
a polymorphic class - swap out implementation at runtime - the Bridge Pattern.
Back to Exception Safety - vector::emplace_back
. Gives us the strong guaranteed. How?
Easy case: No resize, just put the object in the array. When resizing:
- Allocate a new, larger array.
- Invoke copy constructor 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 wanted 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.??? Wrong right
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.
Next: CS247 Lecture 23