Professional C__ - Marc Gregoire [156]
What Are Exceptions, Anyway?
Exceptions are a mechanism for a piece of code to notify another piece of code of an “exceptional” situation or error condition without progressing through the normal code paths. The code that encounters the error throws the exception, and the code that handles the exception catches it. Exceptions do not follow the fundamental rule of step-by-step execution to which you are accustomed. When a piece of code throws an exception, the program control immediately stops executing code step by step and transitions to the exception handler, which could be anywhere from the next line in the same function to several function calls up the stack. If you like sports analogies, you can think of the code that throws an exception as an outfielder throwing a baseball back to the infield, where the nearest infielder (closest exception handler) catches it. Figure 10-1 shows a hypothetical stack of three function calls. Function A() has the exception handler. It calls function B(), which calls function C(), which throws the exception.
FIGURE 10-1
Figure 10-2 shows the handler catching the exception. The stack frames for C() and B() have been removed, leaving only A().
FIGURE 10-2
Some people who have used C++ for years are surprised to learn that C++ supports exceptions. Programmers tend to associate exceptions with languages like Java, in which they are much more visible. However, C++ has full-fledged support for exceptions.
Why Exceptions in C++ Are a Good Thing
As mentioned earlier, run-time errors in programs are inevitable. Despite that fact, error handling in most C and C++ programs is messy and ad hoc. The de facto C error-handling standard, which was carried over into many C++ programs, uses integer function return codes and the errno macro to signify errors. Each thread has its own errno value. errno acts as a thread-global integer variable that functions can use to communicate errors back to calling functions.
Unfortunately, the integer return codes and errno are used inconsistently. Some functions might choose to return 0 for success and −1 for an error. If they return −1, they also set errno to an error code. Other functions return 0 for success and nonzero for an error, with the actual return value specifying the error code. These functions do not use errno. Still others return 0 for failure instead of for success, presumably because 0 always evaluates to false in C and C++.
These inconsistencies can cause problems because programmers encountering a new function often assume that its return codes are the same as other similar functions. That is not always true. On Solaris 9, there are two different libraries of synchronization objects: the POSIX version and the Solaris version. The function to initialize a semaphore in the POSIX version is called sem_init(), and the function to initialize a semaphore in the Solaris version is called sema_init(). As if that weren’t confusing enough, the two functions handle error codes differently! sem_init() returns –1 and sets errno on error, while sema_init() returns the error code directly as a positive integer, and does not set errno.
Another problem is that the return type of functions in C++ can only be of one type, so if you need to return both an error and a value, you must find an alternative mechanism. One solution is to return a std::pair or std::tuple, an object that you can use to store two or more types. They are discussed in later STL chapters. Another choice is to define your own struct or class that contains several values, and return an instance of that struct or class from your function. Yet another option is to return the value or error through a reference parameter or to make the error code one possible value of the return type, such as a nullptr pointer. In all these solutions, the caller is responsible to explicitly check for any errors returned from the function and if it doesn’t handle the error itself, it should propagate the error to