Cippi handles errors.
First of all, according to the C++ Core Guidelines, the following actions are involved in error handling:
Detect an error.
Transmit information about an error to some handler code.
Preserve the valid state of a program.
Avoid resource leaks.
You should use exceptions for error handling. David Abrahams, one of the founders of the Boost C++ Library and former member of the ISO C++ standardization committee, formalized in the document “Exception Safety in Generic Components” what exception safety means. “Abrahams guarantees” describe a contract that is fundamental if you think about exception safety. Here are the four levels of the contract1:
No-throw guarantee, also known as failure transparency: Operations are guaranteed to succeed and satisfy all requirements, even in exceptional situations. If an exception occurs, it is handled internally and cannot be observed by clients.
Strong exception safety, also known as commit or rollback semantics: Operations can fail, but failed operations are guaranteed to have no side effects, so all data retain their original values.
Basic exception safety, also known as a no-leak guarantee: Partial execution of failed operations can cause side effects, but all invariants are preserved, and there are no resource leaks (including memory leaks). Any stored data contains valid values, even if they differ from what they were before the exception.
No exception safety: No guarantees are made.
The rules from the guidelines should help you to avoid the following kinds of errors. I added typical examples in parentheses:
Type violations (casts)
Resource leaks (memory leaks)
Bounds errors (accessing a container outside the boundaries)
Lifetime errors (accessing an object after deletion)
Logical errors (logical expressions)
Interface errors (passing wrong values in interfaces)
1. Source: Bjarne Stroustrup, The C++ Programming Language, Third Edition. Addison-Wesley, 1997.
There are more than 20 rules divided into three categories. The first two categories are about the design of the error-handling strategy and its concrete implementation. The third category discusses situations where you can’t throw an exception.
This section on error handling has a massive overlap with the sections for functions and classes and class hierarchies. I intentionally skipped all rules that I already presented in those sections. The “Related rules” section provides you with the details on the skipped rules.
Each software unit has two communication channels to its client: one for the regular case and one for the irregular case. The software units should be designed around invariants.
First of all, what is a software unit? A software unit may be a function, an object, a subsystem, or the entire system. The software units communicate with their clients. Designing the communication should, therefore, occur early in the design of your system. At the boundary level, you have two ways to communicate: regularly and irregularly. The regular communication is the functional aspect of the interface or, to say it differently, what the software unit should do. The irregular communication stands for the nonfunctional aspects. The nonfunctional aspects specify how a system should operate. A big part of the nonfunctional aspects is error handling, or what can go wrong. Often the nonfunctional aspects are just called “quality attributes.”
From the control-flow perspective, explicit try/catch
has a lot in common with the goto
statement. This means that if an exception is thrown, the control flow directly jumps to the exception handler, which might be in a different software unit. In the end, you may get spaghetti code, meaning code with control flow that is difficult to predict and maintain.
Now, the question is, How should you structure your exception handling? I think you should ask yourself the question, Is it possible to handle the exception locally?
If yes, do it. If not, let the exception propagate until you have sufficient context to handle it. Handling an exception can also mean catching it and then rethrowing a different exception more convenient to the client. This translation of an exception may serve the purpose that the client of the software unit has only to deal with a limited number of different exceptions.
Often boundaries are the appropriate place to handle exceptions because you want to protect the client from arbitrary exceptions. Consequently, boundaries are also the appropriate place to test regular and irregular communication.
E.2: Throw an exception to signal that a function can’t perform its assigned task
E.5: Let a constructor establish an invariant, and throw if it cannot
According to the C++ Core Guidelines, “An invariant is a logical condition for the members of an object that a constructor must establish for the public member functions to assume. After the invariant is established (typically by a constructor) every member function can be called for the object.” This definition is too narrow for me. An invariant can also be established by a function using concepts or contracts.
There are more rules about invariants and how to establish an invariant that complement the discussion at the beginning of this chapter:
C.2: Use class
if the class has an invariant; use struct
if the data members can vary independently
C.41: A constructor should create a fully initialized object
The Core Guidelines’ definition essentially says that you should define your error-handling strategy around invariants. If an invariant can’t be established, throw an exception.
When implementing the error handling, you have to keep a few do’s and don’ts in mind.
Besides the do’s referred to in the section “Related rules” at the end of this chapter, there are three additional rules.
Exceptions are a kind of goto
statement. Maybe your code guidelines forbid you to use goto
statements. Therefore, you come up with a clever idea: Use exceptions for the control flow. In the following example, the exception is used in the success case.
// don't: exception not used for error handling int getIndex(std::vector<const std::string>& vec, const std::string& x) { try { for (auto i = 0; i < vec.size(); ++i) { if (vec[i] == x) throw i; // found x } } catch (int i) { return i; } return -1; // not found }
In my opinion, this is the worst misuse of exceptions. In this case, the regular control flow is not separated from the exceptional control flow. In the success case, the code uses a throw statement; in the failure case, the code uses a return statement. That is confusing, isn’t it?
Use purpose-designed user-defined types as exceptions (not built-in types) |
You should not use built-in types or even the standard exception types. Here are two code snippets from the C++ Core Guidelines exemplifying the don’ts.
void my_code() // Don't { // ... throw 7; // 7 means "moon in the 4th quarter" // ... } void your_code() // Don't { try { // ... my_code(); // ... } catch(int i) { // i == 7 means "input buffer too small" // ... } }
In this case, the exception is an int
without any semantics. What 7
means is described in the comment, but it would be better to use a self-describing type. The comment can be wrong. To be sure, you have to consult the documentation to get an idea. You cannot attach any meaningful information to an exception of kind int
. If you have a 7
, I assume you use at least the numbers 1 to 6 for your exception handling, 1
probably meaning an unspecific error and so on. This strategy is way too sophisticated, error prone, and quite hard to read and to maintain.
Let’s use a standard exception instead of an int
.
void my_code() // Don't { // ... throw std::runtime_error{"moon in the 4th quarter"}; // ... } void your_code() // Don't { try { // ... my_code(); // ... } catch(const std::runtime_error&) { // std::runtime_error means // "input buffer too small" // ... } }
Using a standard exception instead of a built-in type is better because you can attach additional information to an exception or build hierarchies of exceptions. This standard exception is better but not good. Why? The exception is too generic. It’s just a std::runtime_error
. Imagine that the function my_code
is part of an input subsystem. If the client of the function catches the exception by std::runtime_error
, it has no idea if it was a generic error such as “input buffer too small” or a subsystem-specific error such as “input device is not connected.”
To overcome these issues, derive your specific exception from std::runtime_error
. Here is a short example to give you the idea:
class InputSubsystemException: public std::runtime_error { const char* what() const noexcept override { return "Provide more details to the exception"; } };
Now the client of the input, subsystem can specifically catch the exception via catch(const InputSubsystemException& ex)
. Additionally, you can refine the exception hierarchy by further deriving from the class InputSubsystemException
.
If you catch an exception from a hierarchy by value, you may become a victim of slicing.
Imagine that you derive a new exception class USBInputException
from Input-SubsystemException
(previous rule “E.14: Use purpose-designed user-defined types as exceptions [not built-in types]”). You then catch the exception by value of type InputSubsystemException
. Now an exception of type USBInputException
is thrown.
void subsystem() { // ... throw USBInputException(); // ... } void clientCode() { try { subsystem(); } catch(InputSubsystemException e) { // slicing may happen // ... } }
By catching the USBInputException
by value to InputSubsystemException
, slicing kicks in and e
has the base type InputSubsystemException
. Read the details on slicing in the guideline “C.67: A polymorphic class should suppress copying”.
To say it explicitly,
Catch your exception by const
reference and only by reference if you want to modify the exception.
If you rethrow an exception e
in the exception handler, just use throw
and not throw e
. In the second case, e
would be copied.
There is a straightforward cure to catch an exception by value: Apply the rule “C.121: If a base class is used as an interface, make it an abstract class.” Making the InputSubsystemException
an abstract base class makes it impossible to catch InputSubsystemException
by value.
In addition to the do’s, the C++ Core Guidelines have three don’ts.
This is the example of direct ownership from the C++ Core Guidelines:
void leak(int x) { // Bad: may leak auto* p = new int{7}; auto* pa = new int[100] if (x < 0) throw Get_me_out_of_here{}; // leaks *p, and *pa // ... delete p; // we may never get here delete [] pa; }
If the throw is fired, the memory is lost and you have a memory leak. The simple solution is to get rid of the ownership and make the C++ run time the direct owner of the object. This means you simply apply RAII: “R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization).”
Just create a local object or at least a guard as a local object. The C++ run time takes care of local objects and, therefore, frees the memory if necessary. Here are three variations of automatic memory management:
void leak(int x) { // Good: does not leak auto p1 = int{7}; auto p = std::make_unique<int>(7); auto pa = std::vector<int>(100); if (x < 0) throw Get_me_out_of_here{}; // ... }
p1
is a local, but p
and pa
are a kind of guard for the underlying objects. The std::vector
uses the heap to manage its data. Additionally, with all three variations, you eliminate the delete call.
First, here is an example of an exception specification:
int use(int arg) throw(X, Y) { // ... auto x = f(arg); // ... }
This means that the function use
may throw an exception of type X
or Y
. If a different exception is thrown, std::terminate
is called.
Dynamic exception specifications with argument throw(X, Y
) and without argument throw()
were deprecated in C++11. Dynamic exception specifications with arguments were removed with C++17; dynamic exception specifications without arguments were removed with C++20. Until C++20, throw()
was equivalent to noexcept
.
Read more details on noexcept
in the rule “E.12: Use noexcept
when exiting a function because of a throw
is impossible or unacceptable.”
An exception is caught according to the first match strategy. This means that the first exception handler that fits for an exception is used. This is the reason why you should structure your exception handler from specific to general. If not, your specific exception handler may never be invoked. In the following example, the DivisionByZeroException
is derived from std::exception
.
try{ // throw an exception (1) } catch(const DivisionByZeroException& ex) { .... } // (2) catch(const std::exception& ex) { .... } // (3) catch(...) { .... } // (4)
In this case, the DivisionByZeroException
(2) is used first for handling the exception thrown in (1). If the specific handler does not fit, all exceptions derived from std::exception
are caught in the following line (3). The last exception handler in line (4) has an ellipsis (...
) and can, therefore, catch all other exceptions.
E.25: If you can’t throw exceptions, simulate RAII for resource management
E.27: If you can’t throw exceptions, use error codes systematically
Let me start with the first rule: “E.25: If you can’t throw exceptions, simulate RAII for resource management.” The idea of RAII is simple. If you have to take care of a resource, put the resource into a class. Use the constructor of the class for the initialization and the destructor for the destruction of the resource. When you create a local instance of the class on the stack, the C++ run time automatically takes care of the resource and you are done. For more information on RAII, read the first rule for resource management: “R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization).”
What does it mean to simulate RAII for resource management? Imagine you have a function func
that exits with an exception if Gadget
can’t be created.
void func(std::string& arg) { Gadget g {arg}; // ... }
If you can’t throw an exception, you should simulate RAII by adding a valid
member function to Gadget
.
error_indicator func(std::string& arg) { Gadget g {arg}; if (!g.valid()) return gadget_construction_error; // ... return 0; // zero indicates "good" }
In this case, the caller has to test the return value of func
.
The rule “E.26: If you can’t throw exceptions, consider failing fast” is straight-forward. If there is no way to recover from an error such as memory exhaustion, fail fast. If you can’t throw an exception, call std::abort
, which causes abnormal program termination.
void f(int n) { // ... p = static_cast<X*>(malloc(n, X)); if (!p) std::abort(); // abort if memory is exhausted // ... }
std::abort
causes an abnormal program termination if you don’t install a signal handler that catches the signal SIGABRT.
When you don’t install a signal handler, the function f
behaves like the following one:
void f(int n) { // ... p = new X[n]; // throw if memory is exhausted // ... }
Now, I write about the abominable keyword goto
in the last rule: “E.27: If you can’t throw exceptions, use error codes systematically.”
In case of an error, you have a few issues to solve, according to the C++ Core Guidelines:
How do you transmit an error indicator out of a function?
How do you release all resources from a function before doing an error exit?
What do you use as an error indicator?
In general, your function should have two return values, the value and the error indicator. Therefore, std::pair
is a good fit. Releasing the resources may easily become a maintenance nightmare, even if you encapsulate the cleanup code in a function.
std::pair<int, error_indicator> user() { Gadget g1 = make_gadget(17); Gadget g2 = make_gadget(17); if (!g1.valid()) { return {0, g1_error}; } if (!g2.valid()) { cleanup(g1); return {0, g2_error}; } // ... if (all_foobar(g1, g2)) { cleanup(g1); cleanup(g2); return {0, foobar_error}; // ... cleanup(g1); cleanup(g2); return {res, 0}; }
Okay, seems to be correct! Or?
Do you recall what DRY stands for? Don’t repeat yourself. Although the cleanup code is encapsulated into functions, the code has a smell of code repetition because the cleanup functions are invoked in various places. How can we get rid of this repetition? Just put the cleanup code at the end of the function and jump to it.
std::pair<int, error_indicator> user() { error_indicator err = 0; Gadget g1 = make_gadget(17); Gadget g2 = make_gadget(17); if (!g1.valid()) { err = g1_error; // (1) goto exit; } if (!g2.valid()) { err = g2_error; // (1) goto exit; } if (all_foobar(g1, g2)) { err = foobar_error; // (1) goto exit; } // ... exit: if (g1.valid()) cleanup(g1); if (g2.valid()) cleanup(g2); return {res, err}; }
Admittedly, with the help of goto,
the overall structure of the function is quite clear. In case of an error, just the error indicator (1) is set. Exceptional circumstances require exceptional actions.
RAII was already the topic of the first rule to resource management: “R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization).” Consequently, I skipped the rule “E.6: Use RAII to prevent leaks.”
The rules “E.7: State your preconditions” and “E.8: State your postconditions” are about contracts, which are not part of C++20. I give a concise introduction to them in Appendix C, Contracts.
The rule “E.12: Use noexcept
when exiting a function because of a throw
is impossible or unacceptable” is already handled in the rule on functions “F.6: If your function may not throw, declare it noexcept
.”
Global state is hard to manage and introduces hidden dependencies: “I.2: Avoid non-const
global variables.” Consequently, rule E.28 applies: “Avoid error handling based on global state (e.g. errno
).”
The rule “E.16: Destructors, deallocation, and swap
must never fail” is already handled in the rules about classes in the sections Failing Destructors and swap
Function in Chapter 5.