Exception safety is not about catching errors; it is about keeping your data consistent when an error occurs. If a function throws an exception halfway through modifying an object, the object can be left in a corrupted state. C++ developers design code to satisfy specific safety guarantees.
Let's look at safety levels and the copy and swap idiom.
The Three Exception Safety Levels
When you write code that can throw exceptions, you should aim to satisfy one of three safety guarantees:
* Basic Guarantee: If an exception is thrown, no memory or resource is leaked. However, the modified objects can be left in a valid but unspecified state. You can safely destroy them, but their values might be altered.
* Strong Guarantee: If an exception is thrown, the state of the program is completely unchanged (as if the function was never called). This provides transaction like success or rollback behavior.
* Nothrow Guarantee: The function promises never to throw an exception under any circumstances. Functions like destructors and move operators must satisfy this guarantee.
The Copy and Swap Idiom
The copy and swap idiom is a design pattern that provides the Strong Exception Safety Guarantee. Instead of modifying the current object directly (which could throw an exception and leave it half updated), you follow these steps:
1. Create a temporary copy of the object.
2. Perform all risky, exception prone modifications on the temporary copy.
3. Once all operations succeed, swap the internal data of the current object with the copy using a no throw swap operation.
If an exception is thrown during step 2, the temporary copy is automatically destroyed, and the original object remains completely untouched.
* The Draft Paper Metaphor: The copy and swap idiom is like signing house paperwork. You write and edit all the clauses on a draft copy. If you spill coffee on it or the deal falls through (an exception is thrown), your money and real house remain untouched. Only when everything is perfect do you sign and swap keys.
The Destructor Nothrow Rule (Double Exception Crashes)
In C++, destructors are implicitly marked noexcept by default. You must never allow an exception to escape from a destructor.
If an exception is thrown inside a destructor while another exception is already active (which happens during stack frame unwinding), the C++ runtime environment cannot handle two concurrent exceptions. It immediately calls std::terminate to abort the application.
If your destructor performs cleanups that can throw exceptions (such as closing databases or writing logs to disk), you must catch and swallow those exceptions inside the destructor scope: