3.8. Error handling

Programmers are expected to create programs that function correctly and run as expected. Programs need to function ‘correctly’ even in the face of unexpected or unusual conditions.

When the unexpected happens, we need to recover as gracefully as possible. Sometimes the best option is to clearly communicate what happened and exit. Abruptly halting or crashing is not generally acceptable.

Error handling involves:

The rest of this section describes simple error reporting strategies that are compatible with C.

One tried and true way to communicate errors is by returning error values from functions. We already have been introduced to the assert macro to handle errors in the section Assertions.

The assert macro is a ‘function-like’ macro that evaluates a boolean expression and aborts the program if the condition is false. The assert macro is most useful for debugging, but be aware that since it can easily be disabled, it is hard to depend on in production software.

The macro NDEBUG, if defined, will disable all the assert functions in a program.

The assert macro takes a single expression. It does not provide a built-in mechanism for a c

Try changing the assertions to generate errors.

Then define NDEBUG and see what happens.

The assert macro does not provide a built-in mechanism for a custom user message, but there are a few tricks we can use to create a compound expression.

While it is treated as a single expressin by assert, it provides a way to insert a customer error message when the assertion fails.

Using the comma operator is one technique. The entire expression is still a single boolean exppression.

After the left-hand side expression is evaluates, it is discarded. Any side-effects from the evaluated expression remain. This is what the comma operator does and why it’s use is generally discouraged. However, in this case, it’s our message and the program does not need it.

Next the right-hand side is evaluated. If the expression evaluates to false, the entire expression, along with the string literal is displayed by assert before the program exits.

Using the relational ‘and’ operator && is another technique. The entire expression is still a single boolean exppression.

If the left-hand side expression is true, then the right-hand side is evaluated, but since it is a non-zero literal, it will always be true.

If the expression evaluates to false, the entire expression, along with the string literal is displayed by assert before the program exits.

How does the assert macro support our error handling goals?

Another facility C++ inherits from C is the errno macro. errno is a preprocessor macro used for error indication. The exact definition is implementation defined, but expands to a modifiable int. Several standard library functions indicate errors by writing positive integers to errno. Typically, the value of errno is set to one of the error codes, listed in the header cerrno as macro constants that begin with the letter E, followed by uppercase letters or digits.

The value of errno is 0 at program startup, and although library functions are allowed to write positive integers to errno whether or not an error occurred, library functions never store 0 in errno.

Print an error if we use the log function incorrectly.

How does errno support our error handling goals?

In all 4 cases, the answer is the same: it’s up to you.

You need to check errno to see if it has been set. It is your resposibility to reset error if needed. No function that sets errno will ever reset it to 0. Any messages communicated are yours. No error messages are automatically generated. It is also your responsibility to preserve the state of your program and cleanup resources that may be partially or improperly allocated.

One big advantage of errno is that for functions that use it, you get a simple error code you can use to recover from an error without the entire program aborting.

3.8.1. Handling multiple errors at once

Each of the previous error handling techniques are simple, but each allows us to communicate only a single error at a time. Sometimes we need to communicate more information.

We could create a data structure to store each error we care about in a bool.

struct my_errors {
   constexpr const bool busy = false;
   constexpr const bool cancelled = false;
   constexpr const bool domain_error = false;
   constexpr const bool invalid = false;
};

However, this approach does have some limitations. There is no easy way, for example to discover that no errors are set, which hopefully is the normal situation for our program. As programmers, we always want the typical uses or our data structures to be as simple as possible. We want the atypical ones to be simple too!

Can we make this easier to work with? Yes.

Once way is to pack all the boolean values into a single variable.

There are several ways to accomplish this. Here we discuss two of them. Both of them use a single bit to represent the true or false state. Starting from the previous code, we change it like this:

constexpr const unsigned error_none = 0;
constexpr const unsigned error_busy = 1;
constexpr const unsigned error_cancelled = 2;
constexpr const unsigned error_domain = 4;
constexpr const unsigned error_invalid = 8;

The values assigned to each of these variables is not coincidence. Each (other than 0) represents an increase in the power of two: \(2^0, 2^1, 2^2, 2^3\). Each of these numbers sets exactly 1 bit in an unsigned int and no others. So now we can use these values to set the bits in the variable we want to use to keep track of errors.

We use unsigned integers and bitwise operators to ‘flag’ each error. This technique is called a bitmask and it has a long history in programming.

Note

Shifting bits into the sign bit of a signed integer type is implementation defined in C++. To avoid surprising behavior, it is a best practice to only use unsigned integer types when manipulating bits.

Set and print some bits.

We can print the value stored in errors, but we don’t really care about the numeric value, we care about the individual bits in the number.

The maybe_unused attribute suppresses warnings about unused variables.

The approach using bitwise operations is simple once you know the tricks, but C++ provides a type that provides the ability to perform the same operations: std::bitset.

We use the same bitwise operators for bitset that we used with unsigned integers. But bitsets provide some additional features that can make them easier to work with.

Set and print some bits in a bitset.

The maybe_unused attribute suppresses warnings about unused variables.


More to Explore

You have attempted of activities on this page