Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 5 Advanced Concepts of Modern C++

Well, but what are the principles that guide us in developing a good error-handling strategy? When is it justified to throw an exception? How do I deal with thrown exceptions? And for what purposes should exceptions never be used? What are the alternatives?

The following sections present some rules, guidelines, and principles that help C++ programmers design and implement a good error-handling strategy.

Prevention Is Better Than Aftercare

A fundamentally good basic strategy for dealing with errors and exceptions is to generally avoid them. The reason for this is obvious: everything that cannot happen does not have to be treated.

Maybe you will say now, well, this is a truism. Of course it is much better to avoid errors or exceptions, but sometimes it is not possible to prevent them. You’re right, it sounds banal at first glance. And yes, especially when using third-party libraries,

accessing databases, or accessing an external system, unforeseeable things can happen. But for your own code, meaning the parts of the system that you can design as you want, you can take appropriate measures to avoid exceptions as far as possible.

David Abrahams, an American programmer, former ISO C++ standardization committee member, and a founding member of Boost C++ Libraries, created an understanding of what is called exception safety and presented it in a paper [Abrahams98] in 1998. The set of contractual guidelines formulated in this paper, which are also known as the “Abrahams Guarantees,” had a significant influence on the design of the C++ Standard Library and how this library deals with exceptions. But these guidelines are not only relevant to low-level library implementers. They can also be considered by software developers who are writing the application code on higher abstraction levels.

Exception safety is part of the interface design. An interface (API) does not only consist of function signatures, that is, a function’s parameters and return types. The exceptions that might be thrown if a function is invoked are also part of its interface. Furthermore, there are three more aspects that must be considered:

•\ Precondition: A condition that must always be true before a function or a class’s method is invoked. If a precondition is violated, no guarantee can be given that the function call leads to the expected result. The function call may succeed, may fail, can cause unwanted side effects, or show undefined behavior.

197

Chapter 5 Advanced Concepts of Modern C++

•\ Invariant: A condition that must always be true during the execution of a function or method. In other words, it is a condition that is true at the beginning and at the end of a function’s execution. A special form of an invariant in object-orientation is a class invariant. If such an invariant is violated, the object (instance) of the class is left behind in an incorrect and inconsistent state after a method call.

•\ Postcondition: A condition that must always be true immediately after the execution of a function or method. If a postcondition

is violated, an error must have occurred during execution of the function or method.

The idea behind exception safety is that functions, or a class and its methods, give their clients a kind of promise, or a guarantee, about invariants, postconditions, and about exceptions that might be thrown or not thrown. There are four levels of exception safety. In the following subsections, I discuss them shortly in increasing order of safety.

No Exception Safety

With this lowest level of exception safety—literally, no exception safety—absolutely nothing is guaranteed. Any occurring exception can have disastrous consequences. For instance, invariants and postconditions of the called function or method are violated, and a portion of your code, for example, an object, is possibly left behind in a corrupted state.

I think that there is no doubt that the code written by you should never ever offer this inadequate level of exception safety! Just pretend that there is no such thing as “no exception safety.” That’s all; there’s nothing more to say about that.

Basic Exception Safety

The basic exception safety guarantee is the guarantee that any piece of code should offer at least. It is also the exception safety level that can be achieved with relatively little implementation effort. This level guarantees the following:

•\ If an exception is thrown during a function or method call, it is ensured that no resources are leaked! This guarantee includes memory resources as well as other resources. This can be achieved by applying RAII pattern (see the section about RAII and smart pointers).

198

 

Chapter 5 Advanced Concepts of Modern C++

•\

If an exception is thrown during a function or method call, all

 

invariants are preserved.

•\

If an exception is thrown during a function or method call, there will

 

be no corruption of data or memory afterward, and all objects are in

 

a healthy and consistent state. However, it is not guaranteed that the

 

data content is the same as before the function or method has been

 

called.

Tip  The strict rule is this: Design your code, especially your classes, such that they guarantee at least the basic exception safety. This should always be the default exception-safety level!

It is important to know that the C++ Standard Library expects all user types to give at least the basic exception guarantee.

Strong Exception Safety

The strong exception safety guarantees everything that is also guaranteed by the basic exception safety level, but also ensures that in case of an exception, the data is recovered exactly as before the function or method was called. In other words, with this exception-­ safety level, we get commit or rollback semantics like in transaction handling on databases.

It is easy to comprehend that this exception-safety level leads to a higher implementation effort and can be costly at runtime. An example of this additional effort is the so-called copy-and-swap idiom that must be used to ensure strong exception safety for copy assignment.

Equipping your whole code with strong exception safety without any good reasons would violate the KISS and YAGNI principles (see Chapter 3). Hence, the guideline regarding this is in the following tip.

Tip  Issue the strong exception safety guarantee for your code only if it is absolutely required or if the implementation efforts are small compared to the benefits you get (see the Copy-and-Swap idiom discussed in Chapter 9).

199

Chapter 5 Advanced Concepts of Modern C++

Of course, if there are certain quality requirements regarding data integrity and data correctness that have to be satisfied, you have to provide the rollback mechanism that is guaranteed through strong exception safety.

The No-Throw Guarantee

This is the highest exception-safety level, also known as failure transparency. Simply speaking, this level means that as a caller of a function or method, you don’t have to worry about exceptions. The function or method call will succeed. Always! It will never throw an exception, because everything is properly handled internally. There will never be violated invariants and postconditions.

This is the all-round carefree package of exception safety, but it is sometimes very difficult or even impossible to achieve, especially in C++. For instance, if you use any kind of dynamic memory allocation inside a function, like operator new, either directly or indirectly (e.g., via std::make_shared<T>), you have absolutely no chance to end up with a successfully processed function after an exception was encountered.

Here are the cases where the no-throw guarantee is either absolutely mandatory or at least explicitly advised:

•\ Destructors of classes must guarantee to be no-throw under all circumstances! The reason is that, among other situations,

destructors are also called while stack unwinding after an exception has been encountered. It would be fatal if another exception would occur during stack unwinding, because the program would terminate immediately.

As a consequence, any operation inside a destructor that deals with allocated resources and tries to close them, like opened files or allocated memory on the heap, must not throw.

•\ Move operations (move constructors and move assignment operators; see the earlier section about move semantics) should guarantee to be no-throw. If a move operation throws an exception, the probability is enormously high that the move has not taken place. Hence, it should be avoided at all costs that implementations of move operations allocate resources via resource allocation techniques that can throw exceptions. Furthermore, it is important to give the no-­throw guarantee for types that are intended to be used with the C++ Standard Library

200

Chapter 5 Advanced Concepts of Modern C++

containers. If the move constructor for an element type in a container doesn’t give a no-throw guarantee (i.e., the move constructor is not declared with the noexcept specifier), then the container will prefer using the copy operations rather than the move operations.

•\ Default constructors should be preferably no-throw. Basically, throwing an exception in a constructor is not desirable, but it is the best way to deal with constructor failures. A “half-constructed object” does highly likely violate invariants. And an object in a corrupt state that violates its class invariants is useless and dangerous. Therefore, there is nothing speaking against throwing an exception in a default constructor when it is unavoidable. However, it is a good design strategy to largely avoid it. Default constructors should be simple. If a default constructor can throw, it is probably doing too many complex things. Hence, when designing a class, you should try to avoid exceptions in the default constructor.

•\ A swap function must guarantee to be no-throw under all circumstances! An expertly implemented swap() function should not allocate any resources (e.g., memory) using memory allocation techniques that potentially can throw exceptions. It would be fatal if swap() can throw, because it can end up with an inconsistent state. The best way to write an exception-safe operator=() is using a non-­ throwing swap() function for its implementation (see the Copy-and-­ Swap idiom in Chapter 9).

NOEXCEPT SPECIFIER AND OPERATOR [C++11]

Prior to C++11, the throw keyword could be in a function’s declaration. It was used to list all exception types in a comma-separated list that a function might directly or indirectly throw, known as the dynamic exception specification. The usage of throw(exceptionType, exceptionType, ...) is deprecated since C++11 and has been finally removed from the standard in C++17! What was still available, but also marked as deprecated since C++11, was the throw() specifier without an exception type list. This has now also been removed from the standard with C++20. Its semantics are now the same as the noexcept(true) specifier.

201