Exception safety refers to what are the reasonable expectation regarding how a function deals with exceptions.

The basic guarantee

The basic guarantee is that no resources are leaked if a function throws an exception and there there is some recovery action that the program can take to continue. Basically this boils down to objects involved ending up at least in such a state that the destructors will clean up resources.

For example the std::vector::insert needs to copy/move values after the insertion point and should an exception be thrown by the value type then the state of the vector is not fully determined from the caller’s point of view. The caller can destroy the vector, and that in turn will destroy the appropriate values and the underlying array and there will be no memory leaks.

A similar situation is with the vector assignment operator. If the destination size is smaller or equal to the source size, then elements can be copied. Should an exception be thrown by the copy of the value type, then only basic guarantee is provided.

No-throw guarantee

The no-throw guarantee is that a function guarantees that it does not throw. It is possible to design entire classes that will not throw, e.g. see the also the fit RAII pattern.

This raises the question: why would an exception be thrown in the first place? The answer is: to handle errors that are expected to be rare.

Certainly when doing low level operations assigning built-in types like int, char, pointers, arrays of those, that can be done without exceptions. Also it’s refreshing to see that for simple microcontroller systems, accessing devices like configuring the serial port involves writing some values at a certain memory location, there is no failure feedback. But as systems get more complex eventually there are errors.

This std::filesystem function throws if it fails to delete a file (e.g. access denied):

1
bool remove(const std::filesystem::path& p);

It can be noexcept, if it returns an error code (as an argument passed by reference in this case:

1
bool remove(const std::filesystem::path& p, std::error_code& ec) noexcept;

This requires that std::filesystem::path already has a zero terminated string that it can pass to the underlying C API to delete the file.

But again, at higher levels eventually rare errors could happen. In particular memory allocations happen so all over the place. What happens if they fail to allocate? This std::filesystem function that recursively removes the content of a folder is not marked noexcept.

1
std::uintmax_t remove_all(const std::filesystem::path& p, std::error_code& ec);

For most of the errors it will set the error code, but for memory allocation failure it will throw std::bad_alloc. And it needs to allocate memory to build paths to children of the parent folder, as the underlying C API only deletes empty folders.

Memory allocations is the reasons why for most containers the copy constructors and assignment could throw.

For the large majority of cases, destructors don’t throw. Usually functions that guarantee to not throw are marked noexcept, but destructors are assumed to be noexcept by default. Destructors that throw are a bad idea, maybe with the exception of at-scope-exit classes (which is a separate topic), and would not play well with standard containers.

In particular the std::thread design terminates in face of errors (such as the underlying thread still running), so strictly speaking it will not throw exceptions (but might terminate for incorrect usage).

For some applications it might be acceptable to terminate on memory allocation errors, though that approach usually comes with the expectation that a level of resilience/recovery exists elsewhere, outside the current process.

The vector destructor does not throw, and it expects that the same is true for value type.

Unlike the destructors, for the move constructor and assignment, the compiler will not assume they are noexcept implicitly, but a good practice is to ensure they are and mark the them noexcept explicitly.

The vector move constructor and assignment do not throw, they just take shuffle pointer values around (except for the move assignment when certain non-default allocators are involved, again this is another topic).

However some implementations of standard containers e.g. Microsoft’s std::list can throw on move constructors. This comes with consequences as we’ll see in the next article.

The strong guarantee

The strong guarantee is that if an exception is thrown, the program state is unchanged: “commit or rollback” semantics.

It can be achieved by taking a copy of the data involved, changing it, then swapping on success.

Some operations can provide strong guarantees without the additional cost.

This is the intent behind the vector push_back:

  • if no reallocation happen, there are no changes in the container if the value type throws when constructed at the end: the strong guarantee
  • if reallocation happen, the strong guarantee is also given if the value type is no-throw moveable or copyable (even if copy can throw), otherwise it provides just the basic guarantee (the vector can be destroyed).

Variations

Guarantees can vary. A component might give stronger guarantees if additional requirements are met. We’ve just seen the example above.

Also, as another example, push_back is not marked noexcept, but if there is spare capacity, and the new value can be moved/copied without throwing, then push_back will not throw.

In generic code, the expectation that when a generic function is used, exceptions thrown by types provided (e.g. as template parameters) are propagated unchanged to the caller: exception neutrality/exception transparency.

All this means that the three rules above are general guidance on the options available, in the end there are often many options available when going down to details.

Historical notes

The current view on exception safety was defined by the likes of David Abrahams in the context of incorporating generic components from the STL library in the standard C++ library. Then questions were raised about the contract when exceptions are thrown given that the user of a generic component can customize it with their own types.

In particular a famous article by Tom Cargill illustrates the confusion at the time when templates and exception were novelty. He investigates exceptions when poping values from a generic stack, but he runs into problems due to a unfortunate combination of requirements and stack interface. That was addressed in the standard stack by having separate methods to get a reference to the last element (top) and removing it (pop), neither which needs to throw.

Observation

In the end, the complexity around exception is all the result of the complexity comes from handling errors. Much simpler code can be written when not caring about errors.

References

David Abrahams: Exception-Safety in Generic Components

Bjarne Stroustrup: Exception Safety: Concepts and Techniques

Tom Cargill: Exception Handling: A False Sense of Security