Exceptions are part of C++. They are thrown by the standard library classes, and sometimes even if we are not really using the standard library. So, unless we are in a very restrictive environment like embedded programming and have exceptions disabled in the compiler, we need to be prepared and deal with the fact that exceptions simply can happen.

The four levels

Any piece of code we write has one of four levels of exception safety: No guarantee, the basic guarantee, the strong guarantee anf the nothrow guarantee. Let’s consider them one by one.

What does it mean if code has no guarantee regarding exceptions? It simply means that if an exception is thrown during the execution of that piece of code, anything can happen. With “anything” I mean anything bad, from leaked resources to dangling pointers to violated class invariants. Here’s a very simple example:

struct DoubleOwnership { std::unique_ptr<int> pi; std::unique_ptr<double> pd; DoubleOwnership(int* pi_, double* pd_) : pi{pi_}, pd{pd_} {} }; int foo() { DoubleOwnership object { new int(42), new double(3.14) }; //... }

At first glance this may look good, since the object passes both pointers straight to the two `unique_ptr`s that take care of the memory release. But this code may leak memory, since when the second of the two `new`s fails, it will throw a `std::bad_alloc`. The exception will propagate out of the function while the memory allocated by the first `new` has not been given to a `unique_ptr` and therefore will never be freed.

Arguably, when the allocation of memory for something tiny like an `int` or `double` fails, we are in big trouble anyways, but the point is that this code may leak resources and is therefore not exception safe.

Generally, any code that has not been proven to be exception safe should has no guarantee and should be considered unsafe. Code without any exception guarantee is hard to work with – we can’t know for sure the state of the objects after an exception is thrown, which means that we possibly even can’t properly clean up and destroy them.

Don’t write code that has no exception guarantee.

Easier said than done? Not really, because the basic guarantee really is pretty basic. It says that if an exception is thrown during the execution of our code, no resources are leaked and we can be sure that our objects class invariants are not violated. Nothing more, nothing less.

It does especially mean that we don’t necessarily know the content or state or values of our objects, but we know we can use and destroy them, because the invariants are intact. That we can destroy them is probably the most important part of the basic guarantee, since a thrown exception will incur some stack unwinding and affected objects may get destroyed.

Design your classes to have proper class invariants that are always met, even in the presence of exceptions.

The strong guarantee adds to the basic guarantee, that if an operation fails with an exception, then it leaves the objects in the same state they had before. In general, for the strong guarantee we have to do all actions that could possibly throw without affecting any existing object, and then commit them with actions that are guaranteed to not throw an exception.

An example for the strong guarantee is the copy and swap idiom for assignment operators:

Strong& operator=(Strong const& other) { Strong temp(other); temp.swap(*this); return *this; }

The steps are simple: first create a copy of the other object. This may throw an exception, but if it does, the function is terminated early and nothing has happened to `*this` or the other object yet. Then swap `*this` with the copy. For this to work, the swap operation may not throw any exceptions. Examples are the exchange of a few pointers and other built in types. The swap is the commit action, after that the assignment is complete. When the function is left with the return statement, the temporary object is destroyed, cleaning up the state previously owned by `*this`.

Providing the strong guarantee can be costly. For example, imagine if the `Strong` object in the example allocates large amounts of memory. Instead of reusing the already allocated memory, the temporary has to allocate new memory just to release the old one after the swap.

Provide the strong guarantee only if needed. Document operations that have the strong guarantee, use the basic guarantee as default.

The last missing level is the nothrow guarantee. It simply means that an operation can not throw an exception. As you have seen, nothrow operations are needed to provide the strong and basic guarantee. There are some operations that should never throw, no matter what:

destructors have to be nothrow, because they are called during stack unwinding. If an exception is active and a second exception is thrown during stack unwinding, the program will be terminated.

Any cleanup operations like closing files, releasing memory and anything else that might be called from a destructor should not throw.

swap operations. They are commonly expected not to throw. If you have an operation that exchanges the values of two objects but can’t provide the nothrow guarantee, don’t call it `swap` but something different like `exchange`.

Consider using the keyword `noexcept` to document operations that provide the nothrow guarantee.

Conclusion

Reasoning about exception safety can be hard, but thinking in the four levels no guarantee, basic guarantee, strong guarantee and nothrow guarantee makes it much easier. Have a short look at each function you write and make sure that it has at least the basic guarantee. If you use code you have not written, assume it has the basic guarantee as well unless it is documented otherwise.