14
Handling Errors

ERRORS AND EXCEPTIONS

No program exists in isolation; they all depend on external facilities such as interfaces with the operating system, networks and file systems, external code such as third-party libraries, and user input. Each of these areas can introduce situations that require you to respond to problems you may encounter. These potential problems can be referred to with the general term exceptional situations. Even perfectly written programs encounter errors and exceptional situations. Thus, anyone who writes a computer program must include error-handling capabilities. Some languages, such as C, do not include many specific language facilities for error handling. Programmers using these languages generally rely on return values from functions and other ad hoc approaches. Other languages, such as Java, enforce the use of a language feature called exceptions as an error-handling mechanism. C++ lies between these extremes. It provides language support for exceptions, but does not require their use. However, you can’t ignore exceptions entirely in C++ because a few basic facilities, such as memory allocation routines, use them.

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 14-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.

Illustration of a hypothetical stack of three function calls: Function A(), function B(), and function C().

FIGURE 14-1

Figure 14-2 shows the handler catching the exception. The stack frames for C() and B() have been removed, leaving only A().

Illustration of the handler catching the exception.

FIGURE 14-2

Most modern programming languages, such as C# and Java, have support for exceptions, so it’s no surprise that C++ has full-fledged support for them as well. However, if you are coming from C, then exceptions are something new; but once you get used to them, you probably won’t want to go back.

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-local 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. For example, 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 an std::pair or std::tuple, an object that you can use to store two or more types. These classes are discussed in the upcoming chapters that cover the Standard Library. 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 its caller. Unfortunately, this often results in the loss of critical details about the error.

C programmers may be familiar with a mechanism known as setjmp()/longjmp(). This mechanism cannot be used correctly in C++, because it bypasses scoped destructors on the stack. You should avoid it at all costs, even in C programs; therefore, this book does not explain the details of how to use it.

Exceptions provide an easier, more consistent, and safer mechanism for error handling. There are several specific advantages of exceptions over the ad hoc approaches in C and C++.

  • When return codes are used as an error reporting mechanism, you might forget to check the return code and properly handle it either locally or by propagating it upward. The C++17 [[nodiscard]] attribute, introduced in Chapter 11, offers a possible solution to prevent return codes from being ignored, but it’s not foolproof either. Exceptions cannot be forgotten or ignored: if your program fails to catch an exception, it terminates.
  • When integer return codes are used, they generally do not contain sufficient information. You can use exceptions to pass as much information as you want from the code that finds the error to the code that handles it. Exceptions can also be used to communicate information other than errors, though many programmers consider that an abuse of the exception mechanism.
  • Exception handling can skip levels of the call stack. That is, a function can handle an error that occurred several function calls down the stack, without error-handling code in the intermediate functions. Return codes require each level of the call stack to clean up explicitly after the previous level.

In some compilers (fewer and fewer these days), exception handling added a tiny amount of overhead to any function that had an exception handler. For most modern compilers there is a trade-off in that there is almost no overhead in the non-throwing case, and only some slight overhead when you actually throw something. This trade-off is not a bad thing because throwing an exception should be exceptional.

Exception handling is not enforced in C++. For example, in Java a function that does not specify a list of possible exceptions that it can throw is not allowed to throw any exceptions. In C++, it is just the opposite: a function can throw any exception it wants, unless it specifies that it will not throw any exceptions (using the noexcept keyword)!

Recommendation

I recommend exceptions as a useful mechanism for error handling. I feel that the structure and error-handling formalization that exceptions provide outweigh the less desirable aspects. Thus, the remainder of this chapter focuses on exceptions. Also, many popular libraries, such as the Standard Library and Boost, use exceptions, so you need to be prepared to handle them.

EXCEPTION MECHANICS

Exceptional situations arise frequently in file input and output. The following is a function to open a file, read a list of integers from the file, and return the integers in an std::vector data structure. The lack of error handling should jump out at you.

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    // Read the integers one-by-one and add them to a vector.
    vector<int> integers;
    int temp;
    while (inputStream >> temp) {
        integers.push_back(temp);
    }
    return integers;
}

The following line keeps reading values from the ifstream until the end of the file is reached or until an error occurs:

while (inputStream >> temp) {

If the >> operator encounters an error, it sets the fail bit of the ifstream object. In that case, the bool() conversion operator returns false and the while loop terminates. Streams are discussed in more detail in Chapter 13.

You might use readIntegerFile() like this:

const string fileName = "IntegerFile.txt";
vector<int> myInts = readIntegerFile(fileName);
for (const auto& element : myInts) {
    cout << element << " ";
}
cout << endl;

The rest of this section shows you how to add error handling with exceptions, but first, we need to delve a bit deeper into how you throw and catch exceptions.

Throwing and Catching Exceptions

Using exceptions consists of providing two parts in your program: a try/catch construct to handle an exception, and a throw statement that throws an exception. Both must be present in some form to make exceptions work. However, in many cases, the throw happens deep inside some library (including the C++ runtime) and the programmer never sees it, but still has to react to it using a try/catch construct.

The try/catch construct looks like this:

try {
    // ... code which may result in an exception being thrown
} catch (exception-type1 exception-name) {
    // ... code which responds to the exception of type 1
} catch (exception-type2 exception-name) {
    // ... code which responds to the exception of type 2
}
// ... remaining code

The code that may result in an exception being thrown might contain a throw directly. It might also be calling a function that either directly throws an exception or calls—by some unknown number of layers of calls—a function that throws an exception.

If no exception is thrown, the code in the catch blocks is not executed, and the “remaining code” that follows will follow the last statement executed in the try block.

If an exception is thrown, any code following the throw or following the call that resulted in the throw, is not executed; instead, control immediately goes to the right catch block, depending on the type of the exception that is thrown.

If the catch block does not do a control transfer—for example, by returning a value, throwing a new exception, or rethrowing the exception—then the “remaining code” is executed after the last statement of that catch block.

The simplest example to demonstrate exception handling is avoiding division-by-zero. This example throws an exception of type std::invalid_argument, which requires the <stdexcept> header:

double SafeDivide(double num, double den)
{
    if (den == 0)
        throw invalid_argument("Divide by zero");
    return num / den;
}

int main()
{
    try {
        cout << SafeDivide(5, 2) << endl;
        cout << SafeDivide(10, 0) << endl;
        cout << SafeDivide(3, 3) << endl;
    } catch (const invalid_argument& e) {
        cout << "Caught exception: " << e.what() << endl;
    }
    return 0;
}

The output is as follows:

2.5
Caught exception: Divide by zero

throw is a keyword in C++, and is the only way to throw an exception. The invalid_argument() part of the throw line means that you are constructing a new object of type invalid_argument to throw. It is one of the standard exceptions provided by the C++ Standard Library. All Standard Library exceptions form a hierarchy, which is discussed later in this chapter. Each class in the hierarchy supports a what() method that returns a const char* string describing the exception1. This is the string you provide in the constructor of the exception.

Let’s go back to the readIntegerFile() function. The most likely problem to occur is for the file open to fail. That’s a perfect situation for throwing an exception. This code throws an exception of type std::exception, which requires the <exception> header. The syntax looks like this:

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        throw exception();
    }

    // Read the integers one-by-one and add them to a vector
    vector<int> integers;
    int temp;
    while (inputStream >> temp) {
        integers.push_back(temp);
    }
    return integers;
}

If the function fails to open the file and executes the throw exception(); line, the rest of the function is skipped, and control transitions to the nearest exception handler.

Throwing exceptions in your code is most useful when you also write code that handles them. Exception handling is a way to “try” a block of code, with another block of code designated to react to any problems that might occur. In the following main() function, the catch statement reacts to any exception of type exception that was thrown within the try block by printing an error message. If the try block finishes without throwing an exception, the catch block is skipped. You can think of try/catch blocks as glorified if statements. If an exception is thrown in the try block, execute the catch block. Otherwise, skip it.

int main()
{
    const string fileName = "IntegerFile.txt";
    vector<int> myInts;
    try {
        myInts = readIntegerFile(fileName);
    } catch (const exception& e) {
        cerr << "Unable to open file " << fileName << endl;
        return 1;
    }
    for (const auto& element : myInts) {
        cout << element << " ";
    }
    cout << endl;
    return 0;
}

Exception Types

You can throw an exception of any type. The preceding example throws an object of type std::exception, but exceptions do not need to be objects. You could throw a simple int like this:

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        throw 5;
    }
    // Omitted for brevity
}

You would then need to change the catch statement as well:

try {
    myInts = readIntegerFile(fileName);
} catch (int e) {
    cerr << "Unable to open file " << fileName << " (" << e << ")" << endl;
    return 1;
}

Alternatively, you could throw a const char* C-style string. This technique is sometimes useful because the string can contain information about the exception.

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        throw "Unable to open file";
    }
    // Omitted for brevity
}

When you catch the const char* exception, you can print the result:

try {
    myInts = readIntegerFile(fileName);
} catch (const char* e) {
    cerr << e << endl;
    return 1;
}

Despite the previous examples, you should generally throw objects as exceptions for two reasons:

  • Objects convey information by their class name.
  • Objects can store information, including strings that describe the exceptions.

The C++ Standard Library defines a number of predefined exception classes, and you can write your own exception classes, as you’ll learn later in this chapter.

Catching Exception Objects by const Reference

In the preceding example in which readIntegerFile() throws an object of type exception, the catch line looks like this:

} catch (const exception& e) {

However, there is no requirement to catch objects by const reference. You could catch the object by value like this:

} catch (exception e) {

Alternatively, you could catch the object by non-const reference:

} catch (exception& e) { 

Also, as you saw in the const char* example, you can catch pointers to exceptions, as long as pointers to exceptions are thrown.

Throwing and Catching Multiple Exceptions

Failure to open the file is not the only problem readIntegerFile() could encounter. Reading the data from the file can cause an error if it is formatted incorrectly. Here is an implementation of readIntegerFile() that throws an exception if it cannot either open the file or read the data correctly. This time, it uses a runtime_error, derived from exception, and which allows you to specify a descriptive string in its constructor. The runtime_error exception class is defined in <stdexcept>.

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        throw runtime_error("Unable to open the file.");
    }

    // Read the integers one-by-one and add them to a vector
    vector<int> integers;
    int temp;
    while (inputStream >> temp) {
        integers.push_back(temp);
    }

    if (!inputStream.eof()) {
        // We did not reach the end-of-file.
        // This means that some error occurred while reading the file.
        // Throw an exception.
        throw runtime_error("Error reading the file.");
    }

    return integers;
}

Your code in main() does not need to change because it already catches an exception of type exception, from which runtime_error derives. However, that exception could now be thrown in two different situations.

try {
    myInts = readIntegerFile(fileName);
} catch (const exception& e) {
    cerr << e.what() << endl;
    return 1;
}

Alternatively, you could throw two different types of exceptions from readIntegerFile(). Here is an implementation of readIntegerFile() that throws an exception object of class invalid_argument if the file cannot be opened, and an object of class runtime_error if the integers cannot be read. Both invalid_argument and runtime_error are classes defined in the header file <stdexcept> as part of the C++ Standard Library.

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        throw invalid_argument("Unable to open the file.");
    }

    // Read the integers one-by-one and add them to a vector
    vector<int> integers;
    int temp;
    while (inputStream >> temp) {
        integers.push_back(temp);
    }

    if (!inputStream.eof()) {
        // We did not reach the end-of-file.
        // This means that some error occurred while reading the file.
        // Throw an exception.
        throw runtime_error("Error reading the file.");
    }

    return integers;
}

There are no public default constructors for invalid_argument and runtime_error, only string constructors.

Now main() can catch both invalid_argument and runtime_error with two catch statements:

try {
    myInts = readIntegerFile(fileName);
} catch (const invalid_argument& e) {
    cerr << e.what() << endl;
    return 1;
} catch (const runtime_error& e) {
    cerr << e.what() << endl;
    return 2;
}

If an exception is thrown inside the try block, the compiler matches the type of the exception to the proper catch handler. So, if readIntegerFile() is unable to open the file and throws an invalid_argument object, it is caught by the first catch statement. If readIntegerFile() is unable to read the file properly and throws a runtime_error, then the second catch statement catches the exception.

Matching and const

The const-ness specified in the type of the exception you want to catch makes no difference for matching purposes. That is, this line matches any exception of type runtime_error:

} catch (const runtime_error& e) {

The following line also matches any exception of type runtime_error:

} catch (runtime_error& e) {

Matching Any Exception

You can write a catch line that matches any possible exception with the special syntax shown in the following example:

try {
    myInts = readIntegerFile(fileName);
} catch (...) {
    cerr << "Error reading or opening file " << fileName << endl;
    return 1;
}

The three dots are not a typo. They are a wildcard that matches any exception type. When you are calling poorly documented code, this technique can be useful to ensure that you catch all possible exceptions. However, in situations where you have complete information about the set of thrown exceptions, this technique is not recommended because it handles every exception type identically. It’s better to match exception types explicitly and take appropriate, targeted actions.

A possible use of a catch block matching any exception is as a default catch handler. When an exception is thrown, a catch handler is looked up in the order that it appears in the code. The following example shows how you can write catch handlers that explicitly handle invalid_argument and runtime_error exceptions, and how to include a default catch handler for all other exceptions:

try {
    // Code that can throw exceptions
} catch (const invalid_argument& e) {
    // Handle invalid_argument exception
} catch (const runtime_error& e) {
    // Handle runtime_error exception
} catch (...) {
    // Handle all other exceptions
}

Uncaught Exceptions

If your program throws an exception that is not caught anywhere, the program terminates. Basically, there is a try/catch construct around the call to your main() function, which catches all unhandled exceptions, similar to the following:

try {
    main(argc, argv);
} catch (...) {
    // issue error message and terminate program
}

However, this behavior is not usually what you want. The point of exceptions is to give your program a chance to handle and correct undesirable or unexpected situations.

It is also possible to change the behavior of your program if there is an uncaught exception. When the program encounters an uncaught exception, it calls the built-in terminate() function, which calls abort() from <cstdlib> to kill the program. You can set your own terminate_handler by calling set_terminate() with a pointer to a callback function that takes no arguments and returns no value. terminate(), set_terminate(), and terminate_handler are all declared in the <exception> header. The following code shows a high-level overview of how it works:

try {
    main(argc, argv);
} catch (...) {
    if (terminate_handler != nullptr) {
        terminate_handler();
    } else {
        terminate();
    }
}
// normal termination code

Before you get too excited about this feature, you should know that your callback function must still terminate the program. It can’t just ignore the error. However, you can use it to print a helpful error message before exiting. Here is an example of a main() function that doesn’t catch the exceptions thrown by readIntegerFile(). Instead, it sets the terminate_handler to a custom callback. This callback prints an error message and terminates the process by calling exit(). The exit() function accepts an integer which is returned back to the operating system, and which can be used to determine how a process exited.

void myTerminate()
{
    cout << "Uncaught exception!" << endl;
    exit(1);
}

int main()
{
    set_terminate(myTerminate);

    const string fileName = "IntegerFile.txt";
    vector<int> myInts = readIntegerFile(fileName);

    for (const auto& element : myInts) {
        cout << element << " ";
    }
    cout << endl;
    return 0;
}

Although not shown in this example, set_terminate() returns the old terminate_handler when it sets the new one. The terminate_handler applies program-wide, so it’s considered good style to reset the old terminate_handler when you have completed the code that needed the new terminate_handler. In this case, the entire program needs the new terminate_handler, so there’s no point in resetting it.

While it’s important to know about set_terminate(), it’s not a very effective exception-handling approach. It’s recommended to try to catch and handle each exception individually in order to provide more precise error handling.

noexcept

A function is allowed to throw any exception it likes. However, it is possible to mark a function with the noexcept keyword to state that it will not throw any exceptions. For example, here is the readIntegerFile() function from earlier, but this time marked as noexcept, so it is not allowed to throw any exceptions:

vector<int> readIntegerFile(string_view fileName) noexcept;

When a function marked as noexcept throws an exception anyway, C++ calls terminate() to terminate the application.

When you override a virtual method in a derived class, you are allowed to mark the overridden method as noexcept, even if the version in the base class is not noexcept. The opposite is not allowed.

Throw Lists (Deprecated/Removed)

Older versions of C++ allowed you to specify the exceptions a function or method intended to throw. This specification was called the throw list or the exception specification.

Because C++17 has officially removed support for exception specifications, this book does not use them, and does not explain them in detail. Even while exception specifications were still supported, they were used rarely. Still, this section briefly mentions them, so you will at least get an idea of what the syntax looked like in the unlikely event you encounter them in legacy code. Here is a simple example of the syntax, the readIntegerFile() function from earlier, with an exception specification:

vector<int> readIntegerFile(string_view fileName)
    throw(invalid_argument, runtime_error)
{
    // Remainder of the function is the same as before
}

If a function threw an exception that was not in its exception specification, the C++ runtime called std::unexpected() which by default called std::terminate() to terminate the application.

EXCEPTIONS AND POLYMORPHISM

As described earlier, you can actually throw any type of exception. However, classes are the most useful types of exceptions. In fact, exception classes are usually written in a hierarchy, so that you can employ polymorphism when you catch the exceptions.

The Standard Exception Hierarchy

You’ve already seen several exceptions from the C++ standard exception hierarchy: exception, runtime_error, and invalid_argument. Figure 14-3 shows the full hierarchy. For completeness, all standard exceptions are shown, including those thrown by parts of the Standard Library, which are discussed in later chapters.

Illustration of Standard Exception Hierarchy.

FIGURE 14-3

All of the exceptions thrown by the C++ Standard Library are objects of classes in this hierarchy. Each class in the hierarchy supports a what() method that returns a const char* string describing the exception. You can use this string in an error message.

Most of the exception classes (a notable exception is the base exception class) require you to set in the constructor the string that is returned by what(). That’s why you have to specify a string in the constructors for runtime_error and invalid_argument. This has already been done in examples throughout this chapter. Here is another version of readIntegerFile() that includes the filename in the error message. Note also the use of the standard user-defined literal “s”, introduced in Chapter 2, to interpret a string literal as an std::string.

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        const string error = "Unable to open file "s + fileName.data();
        throw invalid_argument(error);
    }

    // Read the integers one-by-one and add them to a vector
    vector<int> integers;
    int temp;
    while (inputStream >> temp) {
        integers.push_back(temp);
    }

    if (!inputStream.eof()) {
        // We did not reach the end-of-file.
        // This means that some error occurred while reading the file.
        // Throw an exception.
        const string error = "Unable to read file "s + fileName.data();
        throw runtime_error(error);
    }

    return integers;
}

int main()
{
    // Code omitted
    try {
        myInts = readIntegerFile(fileName);
    } catch (const invalid_argument& e) {
        cerr << e.what() << endl;
        return 1;
    } catch (const runtime_error& e) {
        cerr << e.what() << endl;
        return 2;
    }
    // Code omitted
}

Catching Exceptions in a Class Hierarchy

A feature of exception hierarchies is that you can catch exceptions polymorphically. For example, if you look at the two catch statements in main() following the call to readIntegerFile(), you can see that they are identical except for the exception class that they handle. Conveniently, invalid_argument and runtime_error are both derived classes of exception, so you can replace the two catch statements with a single catch statement for class exception:

try {
    myInts = readIntegerFile(fileName);
} catch (const exception& e) {
    cerr << e.what() << endl;
    return 2;
}

The catch statement for an exception reference matches any derived classes of exception, including both invalid_argument and runtime_error. Note that the higher in the exception hierarchy you catch exceptions, the less specific your error handling can be. You should generally catch exceptions at as specific a level as possible.

When more than one catch clause is used, the catch clauses are matched in syntactic order as they appear in your code; the first one that matches, wins. If one catch is more inclusive than a later one, it will match first, and the more restrictive one, which comes later, will not be executed at all. Therefore, you should place your catch clauses from most restrictive to least restrictive in order. For example, suppose that you want to catch invalid_argument from readIntegerFile() explicitly, but you also want to leave the generic exception match for any other exceptions. The correct way to do so is like this:

try {
    myInts = readIntegerFile(fileName);
} catch (const invalid_argument& e) { // List the derived class first.
    // Take some special action for invalid filenames.
} catch (const exception& e) { // Now list exception
    cerr << e.what() << endl;
    return 1;
}

The first catch statement catches invalid_argument exceptions, and the second catches any other exceptions of type exception. However, if you reverse the order of the catch statements, you don’t get the same result:

try {
    myInts = readIntegerFile(fileName);
} catch (const exception& e) { // BUG: catching base class first!
    cerr << e.what() << endl;
    return 1;
} catch (const invalid_argument& e) {
    // Take some special action for invalid filenames.
}

With this order, any exception of a class that derives from exception is caught by the first catch statement; the second will never be reached. Some compilers issue a warning in this case, but you shouldn’t count on it.

Writing Your Own Exception Classes

There are two advantages to writing your own exception classes.

  1. The number of exceptions in the C++ Standard Library is limited. Instead of using an exception class with a generic name, such as runtime_error, you can create classes with names that are more meaningful for the particular errors in your program.
  2. You can add your own information to these exceptions. The exceptions in the standard hierarchy allow you to set only an error string. You might want to pass different information in the exception.

It’s recommended that all the exception classes that you write inherit directly or indirectly from the standard exception class. If everyone on your project follows that rule, you know that every exception in the program will be derived from exception (assuming that you aren’t using third-party libraries that break this rule). This guideline makes exception handling via polymorphism significantly easier.

For example, invalid_argument and runtime_error don’t do a very good job of capturing the file opening and reading errors in readIntegerFile(). You can define your own error hierarchy for file errors, starting with a generic FileError class:

class FileError : public exception
{
    public:
        FileError(string_view fileName) : mFileName(fileName) {}

        virtual const char* what() const noexcept override {
            return mMessage.c_str();
        }

        string_view getFileName() const noexcept { return mFileName; }

    protected:
        void setMessage(string_view message) { mMessage = message; }

    private:
        string mFileName;
        string mMessage;
};

As a good programming citizen, you should make FileError a part of the standard exception hierarchy. It seems appropriate to integrate it as a child of exception. When you derive from exception, you can override the what() method, which has the prototype shown and which must return a const char* string that is valid until the object is destroyed. In the case of FileError, this string comes from the mMessage data member. Derived classes of FileError can set the message using the protected setMessage() method. The generic FileError class also contains a filename, and a public accessor for that filename.

The first exceptional situation in readIntegerFile() occurs if the file cannot be opened. Thus, you might want to write a FileOpenError exception derived from FileError:

class FileOpenError : public FileError
{
    public:
        FileOpenError(string_view fileName) : FileError(fileName)
        {
            setMessage("Unable to open "s + fileName.data());
        }
};

The FileOpenError exception changes the mMessage string to represent the file-opening error.

The second exceptional situation in readIntegerFile() occurs if the file cannot be read properly. It might be useful for this exception to contain the line number of the error in the file, as well as the filename in the error message string returned from what(). Here is a FileReadError exception derived from FileError:

class FileReadError : public FileError
{
    public:
        FileReadError(string_view fileName, size_t lineNumber)
            : FileError(fileName), mLineNumber(lineNumber)
        {
            ostringstream ostr;
            ostr << "Error reading " << fileName << " at line "
                 << lineNumber;
            setMessage(ostr.str());
        }

        size_t getLineNumber() const noexcept { return mLineNumber; }

    private:
        size_t mLineNumber;
};

Of course, in order to set the line number properly, you need to modify your readIntegerFile() function to track the number of lines read instead of just reading integers directly. Here is a new readIntegerFile() function that uses the new exceptions:

vector<int> readIntegerFile(string_view fileName)
{
    ifstream inputStream(fileName.data());
    if (inputStream.fail()) {
        // We failed to open the file: throw an exception
        throw FileOpenError(fileName);
    }

    vector<int> integers;
    size_t lineNumber = 0;
    while (!inputStream.eof()) {
        // Read one line from the file
        string line;
        getline(inputStream, line);
        ++lineNumber;

        // Create a string stream out of the line
        istringstream lineStream(line);

        // Read the integers one-by-one and add them to a vector
        int temp;
        while (lineStream >> temp) {
            integers.push_back(temp);
        }

        if (!lineStream.eof()) {
            // We did not reach the end of the string stream.
            // This means that some error occurred while reading this line.
            // Throw an exception.
            throw FileReadError(fileName, lineNumber);
        }
    }

    return integers;
}

Now, code that calls readIntegerFile() can use polymorphism to catch exceptions of type FileError like this:

try {
    myInts = readIntegerFile(fileName);
} catch (const FileError& e) {
    cerr << e.what() << endl;
    return 1;
}

There is one trick to writing classes whose objects will be used as exceptions. When a piece of code throws an exception, the object or value thrown is moved or copied, using either the move constructor or copy constructor. Thus, if you write a class whose objects will be thrown as exceptions, you must make sure those objects are copyable and/or moveable. This means that if you have dynamically allocated memory, your class must have a destructor, but also a copy constructor and copy assignment operator, and/or a move constructor and move assignment operator, see Chapter 9.

It is possible for exceptions to be copied more than once, but only if you catch the exception by value instead of by reference.

Nested Exceptions

It could happen that during handling of a first exception, a second exceptional situation is triggered which requires a second exception to be thrown. Unfortunately, when you throw the second exception, all information about the first exception that you are currently trying to handle will be lost. The solution provided by C++ for this problem is called nested exceptions, which allow you to nest a caught exception in the context of a new exception. This can also be useful if you call a function in a third-party library that throws an exception of a certain type, A, but you only want exceptions of another type, B, in your code. In such a case, you catch all exceptions from the library, and nest them in an exception of type B.

You use std::throw_with_nested() to throw an exception with another exception nested inside it. A catch handler for the second exception can use a dynamic_cast() to get access to the nested_exception representing the first exception. The following example demonstrates the use of nested exceptions. This example defines a MyException class, which derives from exception and accepts a string in its constructor.

class MyException : public std::exception
{
    public:
        MyException(string_view message) : mMessage(message) {}
        virtual const char* what() const noexcept override {
            return mMessage.c_str();
        }
    private:
        string mMessage;
};

When you are handling a first exception and you need to throw a second exception with the first one nested inside it, you need to use the std::throw_with_nested() function. The following doSomething() function throws a runtime_error that is immediately caught in the catch handler. The catch handler writes a message and then uses the throw_with_nested() function to throw a second exception that has the first one nested inside it. Note that nesting the exception happens automatically. The predefined __func__ variable is discussed in Chapter 1.

void doSomething()
{
    try {
        throw runtime_error("Throwing a runtime_error exception");
    } catch (const runtime_error& e) {
        cout << __func__ << " caught a runtime_error" << endl;
        cout << __func__ << " throwing MyException" << endl;
        throw_with_nested(
            MyException("MyException with nested runtime_error"));
    }
}

The following main() function demonstrates how to handle the exception with a nested exception. The code calls the doSomething() function and has one catch handler for exceptions of type MyException. When it catches such an exception, it writes a message, and then uses a dynamic_cast() to get access to the nested exception. If there is no nested exception inside, the result will be a null pointer. If there is a nested exception inside, the rethrow_nested() method on the nested_exception is called. This will cause the nested exception to be rethrown, which you can then catch in another try/catch block.

int main()
{
    try {
        doSomething();
    } catch (const MyException& e) {
        cout << __func__ << " caught MyException: " << e.what() << endl;

        const auto* pNested = dynamic_cast<const nested_exception*>(&e);
        if (pNested) {
            try {
                pNested->rethrow_nested();
            } catch (const runtime_error& e) {
                // Handle nested exception
                cout << "  Nested exception: " << e.what() << endl;
            }
        }
    }
    return 0;
}

The output should be as follows:

doSomething caught a runtime_error
doSomething throwing MyException
main caught MyException: MyException with nested runtime_error
  Nested exception: Throwing a runtime_error exception

The preceding main() function uses a dynamic_cast() to check for the nested exception. Because you often have to perform this dynamic_cast() if you want to check for a nested exception, the standard provides a small helper function called std::rethrow_if_nested() that does it for you. This helper function can be used as follows:

int main()
{
    try {
        doSomething();
    } catch (const MyException& e) {
        cout << __func__ << " caught MyException: " << e.what() << endl;
        try {
            rethrow_if_nested(e);
        } catch (const runtime_error& e) {
            // Handle nested exception
            cout << "  Nested exception: " << e.what() << endl;
        }
    }
    return 0;
}

RETHROWING EXCEPTIONS

The throw keyword can also be used to rethrow the current exception, as in the following example:

void g() { throw invalid_argument("Some exception"); }

void f()
{
    try {
        g();
    } catch (const invalid_argument& e) {
        cout << "caught in f: " << e.what() << endl;
        throw;  // rethrow
    }
}

int main()
{
    try {
        f();
    } catch (const invalid_argument& e) {
        cout << "caught in main: " << e.what() << endl;
    }
    return 0;
}

This example produces the following output:

caught in f: Some exception
caught in main: Some exception

You might think you could also rethrow an exception using something like a “throw e;” statement; however, that’s wrong, because it can cause slicing of your exception object. For example, suppose f() is modified to catch std::exceptions, and main() is modified to catch both exception and invalid_argument exceptions:

void g() { throw invalid_argument("Some exception"); }

void f()
{
    try {
        g();
    } catch (const exception& e) {
        cout << "caught in f: " << e.what() << endl;
        throw;  // rethrow
    }
}

int main()
{
    try {
        f();
    } catch (const invalid_argument& e) {
        cout << "invalid_argument caught in main: " << e.what() << endl;
    } catch (const exception& e) {
        cout << "exception caught in main: " << e.what() << endl;
    }
    return 0;
}

Remember that invalid_argument derives from exception. The output of this code is as you would expect:

caught in f: Some exception
invalid_argument caught in main: Some exception

However, try replacing the “throw;” line in f() with:

throw e;

The output then is as follows:

caught in f: Some exception
exception caught in main: Some exception

Now main() seems to be catching an exception object, instead of an invalid_argument object. That’s because the “throw e;” statement causes slicing, reducing the invalid_argument to an exception.

STACK UNWINDING AND CLEANUP

When a piece of code throws an exception, it searches for a catch handler on the stack. This catch handler could be zero or more function calls up the stack of execution. When one is found, the stack is stripped back to the stack level that defines the catch handler by unwinding all intermediate stack frames. Stack unwinding means that the destructors for all locally scoped names are called, and all code remaining in each function past the current point of execution is skipped.

However, in stack unwinding, pointer variables are not freed, and other cleanup is not performed. This behavior can present problems, as the following code demonstrates:

void funcOne();
void funcTwo();

int main()
{
    try {
        funcOne();
    } catch (const exception& e) {
        cerr << "Exception caught!" << endl;
        return 1;
    }
    return 0;
}

void funcOne()
{
    string str1;
    string* str2 = new string();
    funcTwo();
    delete str2;
}

void funcTwo()
{
    ifstream fileStream;
    fileStream.open("filename");
    throw exception();
    fileStream.close();
}

When funcTwo() throws an exception, the closest exception handler is in main(). Control then jumps immediately from this line in funcTwo(),

throw exception();

to this line in main():

cerr << "Exception caught!" << endl;

In funcTwo(), control remains at the line that threw the exception, so this subsequent line never gets a chance to run:

fileStream.close();

However, luckily for you, the ifstream destructor is called because fileStream is a local variable on the stack. The ifstream destructor closes the file for you, so there is no resource leak here. If you had dynamically allocated fileStream, it would not be destroyed, and the file would not be closed.

In funcOne(), control is at the call to funcTwo(), so this subsequent line never gets a chance to run:

delete str2;

In this case, there really is a memory leak. Stack unwinding does not automatically call delete on str2 for you. However, str1 is destroyed properly because it is a local variable on the stack. Stack unwinding destroys all local variables correctly.

This is one reason why you should never mix older C models of allocation (even if you are calling new so it looks like C++) with modern programming methodologies like exceptions. In C++, this situation should be handled by using stack-based allocations, or if that is not possible, by one of the techniques discussed in the following two sections.

Use Smart Pointers

If stack-based allocation is not possible, then use smart pointers. They allow you to write code that automatically prevents memory or resource leaks with exception handling. Whenever a smart pointer object is destroyed, it frees the underlying resource. Here is an example of the previous funcOne() function but using the unique_ptr smart pointer, defined in <memory>, and introduced in Chapter 1:

void funcOne() 
{
    string str1;
    auto str2 = make_unique<string>("hello");
    funcTwo();
}

The str2 pointer will automatically be deleted when you return from funcOne() or when an exception is thrown.

Of course, you should only allocate something dynamically if you have a good reason to do so. For example, in the previous funcOne() function, there is no good reason to make str2 a dynamically allocated string. It should just be a stack-based string variable. It’s merely shown here as a compact example of the consequences of throwing exceptions.

Catch, Cleanup, and Rethrow

Another technique for avoiding memory and resource leaks is for each function to catch any possible exceptions, perform necessary cleanup work, and rethrow the exception for the function higher up the stack to handle. Here is a revised funcOne() with this technique:

void funcOne()
{
    string str1;
    string* str2 = new string();
    try {
        funcTwo();
    } catch (...) {
        delete str2;
        throw; // Rethrow the exception.
    }
    delete str2;
}

This function wraps the call to funcTwo() with an exception handler that performs the cleanup (calls delete on str2) and then rethrows the exception. The keyword throw by itself rethrows whatever exception was caught most recently. Note that the catch statement uses the ... syntax to catch any exception.

This method works fine, but is messy. In particular, note that there are now two identical lines that call delete on str2: one to handle the exception and one if the function exits normally.

COMMON ERROR-HANDLING ISSUES

Whether or not you use exceptions in your programs is up to you and your colleagues. However, you are strongly encouraged to formalize an error-handling plan for your programs, regardless of your use of exceptions. If you use exceptions, it is generally easier to come up with a unified error-handling scheme, but it is not impossible without exceptions. The most important aspect of a good plan is uniformity of error handling throughout all the modules of the program. Make sure that every programmer on the project understands and follows the error-handling rules.

This section discusses the most common error-handling issues in the context of exceptions, but the issues are also relevant to programs that do not use exceptions.

Memory Allocation Errors

Despite the fact that all the examples so far in this book have ignored the possibility, memory allocation can fail. On current 64-bit platforms, this will almost never happen, but on mobile or legacy systems, memory allocation can fail. On such systems, you must account for memory allocation failures. C++ provides several different ways to handle memory errors.

The default behaviors of new and new[] are to throw an exception of type bad_alloc, defined in the <new> header file, if they cannot allocate memory. Your code could catch these exceptions and handle them appropriately.

It’s not realistic to wrap all your calls to new and new[] with a try/catch, but at least you should do so when you are trying to allocate a big block of memory. The following example demonstrates how to catch memory allocation exceptions:

int* ptr = nullptr;
size_t integerCount = numeric_limits<size_t>::max();
try {
    ptr = new int[integerCount];
} catch (const bad_alloc& e) {
    cerr << __FILE__ << "(" << __LINE__
         << "): Unable to allocate memory: " << e.what() << endl;
    // Handle memory allocation failure.
    return;
}
// Proceed with function that assumes memory has been allocated.

Note that this code uses the predefined preprocessor symbols __FILE__ and __LINE__, which are replaced with the name of the file and the current line number. This makes debugging easier.

You could, of course, bulk handle many possible new failures with a single try/catch block at a higher point in the program, if that works for your program.

Another point to consider is that logging an error might try to allocate memory. If new fails, there might not be enough memory left even to log the error message.

Non-throwing new

If you don’t like exceptions, you can revert to the old C model in which memory allocation routines return a null pointer if they cannot allocate memory. C++ provides nothrow versions of new and new[], which return nullptr instead of throwing an exception if they fail to allocate memory. This is done by using the syntax new(nothrow) instead of new, as shown in the following example:

int* ptr = new(nothrow) int[integerCount];
if (ptr == nullptr) {
    cerr << __FILE__ << "(" << __LINE__
         << "): Unable to allocate memory!" << endl;
    // Handle memory allocation failure.
    return;
}
// Proceed with function that assumes memory has been allocated.

The syntax is a little strange: you really do write “nothrow” as if it’s an argument to new (which it is).

Customizing Memory Allocation Failure Behavior

C++ allows you to specify a new handler callback function. By default, there is no new handler, so new and new[] just throw bad_alloc exceptions. However, if there is a new handler, the memory allocation routine calls the new handler upon memory allocation failure instead of throwing an exception. If the new handler returns, the memory allocation routines attempt to allocate memory again, calling the new handler again if they fail. This cycle could become an infinite loop unless your new handler changes the situation with one of three alternatives. Practically speaking, some of the options are better than others. Here is the list with commentary:

  • Make more memory available. One trick to expose space is to allocate a large chunk of memory at program start-up, and then to free it in the new handler. A practical example is when you hit an allocation error and you need to save the user state so no work gets lost. The key is to allocate a block of memory at program start-up large enough to allow a complete document save operation. When the new handler is triggered, you free this block, save the document, restart the application, and let it reload the saved document.
  • Throw an exception. The C++ standard mandates that if you throw an exception from your new handler, it must be a bad_alloc exception, or an exception derived from bad_alloc. For example:
    • You could write and throw a document_recovery_alloc exception, inheriting from bad_alloc. This exception can be caught somewhere in your application to trigger the document save operation and restart of the application.
    • You could write and throw a please_terminate_me exception, deriving from bad_alloc. In your top-level function—for example, main()—you catch this exception and handle it by returning from the top-level function. It’s recommended to terminate a program by returning from the top-level function, instead of by calling exit().
  • Set a different new handler. Theoretically, you could have a series of new handlers, each of which tries to create memory and sets a different new handler if it fails. However, such a scenario is usually more complicated than useful.

If you don’t do one of these three things in your new handler, any memory allocation failure will cause an infinite loop.

If there are some memory allocations that can fail but you don’t want the new handler to be called, you can simply set the new handler back to its default of nullptr temporarily before calling new in those cases.

You set the new handler with a call to set_new_handler(), declared in the <new> header file. Here is an example of a new handler that logs an error message and throws an exception:

class please_terminate_me : public bad_alloc { };

void myNewHandler()
{
    cerr << "Unable to allocate memory." << endl;
    throw please_terminate_me();
}

The new handler must take no arguments and return no value. This new handler throws a please_terminate_me exception, as suggested in the second bullet in the preceding list.

You can set this new handler like this:

int main()
{
    try {
        // Set the new new_handler and save the old one.
        new_handler oldHandler = set_new_handler(myNewHandler);

        // Generate allocation error
        size_t numInts = numeric_limits<size_t>::max();
        int* ptr = new int[numInts];

        // Reset the old new_handler
        set_new_handler(oldHandler);
    } catch (const please_terminate_me&) {
        cerr << __FILE__ << "(" << __LINE__
             << "): Terminating program." << endl;
        return 1;
    }
    return 0;
}

Note that new_handler is a typedef for the type of function pointer that set_new_handler() takes.

Errors in Constructors

Before C++ programmers discover exceptions, they are often stymied by error handling and constructors. What if a constructor fails to construct the object properly? Constructors don’t have a return value, so the standard pre-exception error-handling mechanism doesn’t work. Without exceptions, the best you can do is to set a flag in the object specifying that it is not constructed properly. You can provide a method, with a name like checkConstructionStatus(), which returns the value of that flag, and hope that clients remember to call this method on the object after constructing it.

Exceptions provide a much better solution. You can throw an exception from a constructor, even though you can’t return a value. With exceptions, you can easily tell clients whether or not construction of an object succeeded. However, there is one major problem: if an exception leaves a constructor, the destructor for that object will never be called! Thus, you must be careful to clean up any resources and free any allocated memory in constructors before allowing exceptions to leave the constructor. This problem is the same as in any other function, but it is subtler in constructors because you’re accustomed to letting the destructors take care of the memory deallocation and resource freeing.

This section describes a Matrix class template as an example in which the constructor correctly handles exceptions. Note that this example is using a raw pointer called mMatrix to demonstrate the problems. In production-quality code, you should avoid using raw pointers, for example, by using a Standard Library container! The definition of the Matrix class template looks like this:

template <typename T>
class Matrix
{
    public:
        Matrix(size_t width, size_t height);
        virtual ~Matrix();
    private:
        void cleanup();

        size_t mWidth = 0;
        size_t mHeight = 0;
        T** mMatrix = nullptr;
};

The implementation of the Matrix class is as follows. Note that the first call to new is not protected with a try/catch block. It doesn’t matter if the first new throws an exception because the constructor hasn’t allocated anything else yet that needs freeing. If any of the subsequent new calls throws an exception, though, the constructor must clean up all of the memory already allocated. However, it doesn’t know what exceptions the T constructors themselves might throw, so it catches any exceptions via ... and then nests the caught exception inside a bad_alloc exception. Note that the array allocated with the first call to new is zero-initialized using the {} syntax, that is, each element will be nullptr. This makes the cleanup() method easier, because it is allowed to call delete on a nullptr.

template <typename T>
Matrix<T>::Matrix(size_t width, size_t height)
{
    mMatrix = new T*[width] {};    // Array is zero-initialized!

    // Don't initialize the mWidth and mHeight members in the ctor-
    // initializer. These should only be initialized when the above
    // mMatrix allocation succeeds!
    mWidth = width;
    mHeight = height;

    try {
        for (size_t i = 0; i < width; ++i) {
            mMatrix[i] = new T[height];
        }
    } catch (...) {
        std::cerr << "Exception caught in constructor, cleaning up..."
            << std::endl;
        cleanup();
        // Nest any caught exception inside a bad_alloc exception.
        std::throw_with_nested(std::bad_alloc());
    }
}

template <typename T>
Matrix<T>::~Matrix()
{
    cleanup();
}

template <typename T>
void Matrix<T>::cleanup()
{
    for (size_t i = 0; i < mWidth; ++i)
        delete[] mMatrix[i];
    delete[] mMatrix;
    mMatrix = nullptr;
    mWidth = mHeight = 0;
}

The Matrix class template can be tested as follows:

class Element
{
    // Kept to a bare minimum, but in practice, this Element class
    // could throw exceptions in its constructor.
    private:
        int mValue;
};

int main()
{
    Matrix<Element> m(10, 10);
    return 0;
}

You might be wondering what happens when you add inheritance into the mix. Base class constructors run before derived class constructors. If a derived class constructor throws an exception, C++ will execute the destructor of the fully constructed base class.

Function-Try-Blocks for Constructors

The exception mechanism, as discussed up to now in this chapter, is perfect for handling exceptions within functions. But how should you handle exceptions thrown from inside a ctor-initializer of a constructor? This section explains a feature called function-try-blocks, which are capable of catching those exceptions. Function-try-blocks work for normal functions as well as for constructors. This section focuses on the use with constructors. Most C++ programmers, even experienced C++ programmers, don’t know of the existence of this feature, even though it was introduced a long time ago.

The following piece of pseudo-code shows the basic syntax for a function-try-block for a constructor:

MyClass::MyClass()
try
    : <ctor-initializer>
{
     /* ... constructor body ... */
}
catch (const exception& e)
{
     /* ... */
}

As you can see, the try keyword should be right before the start of the ctor-initializer. The catch statements should be after the closing brace for the constructor, actually putting them outside the constructor body. There are a number of restrictions and guidelines that you should keep in mind when using function-try-blocks with constructors:

  • The catch statements catch any exception thrown either directly or indirectly by the ctor-initializer or by the constructor body.
  • The catch statements have to rethrow the current exception or throw a new exception. If a catch statement doesn’t do this, the runtime automatically rethrows the current exception.
  • The catch statements can access arguments passed to the constructor.
  • When a catch statement catches an exception in a function-try-block, all fully constructed base classes and members of the object are destroyed before execution of the catch statement starts.
  • Inside catch statements you should not access member variables that are objects because these are destroyed prior to executing the catch statements (see the previous bullet). However, if your object contains non-class data members—for example, raw pointers—you can access them if they have been initialized before the exception was thrown. If you have such naked resources, you have to take care of them by freeing them in the catch statements, as the following example demonstrates.
  • The catch statements in a function-try-block cannot use the return keyword to return a value from the function enclosed by it. This is not relevant for constructors because they do not return anything.

Based on this list of limitations, function-try-blocks for constructors are useful only in a limited number of situations:

  • To convert an exception thrown by the ctor-initializer to another exception.
  • To log a message to a log file.
  • To free naked resources that have been allocated in the ctor-initializer prior to the exception being thrown.

The following example demonstrates how to use function-try-blocks. The code defines a class called SubObject. It has only one constructor, which throws an exception of type runtime_error.

class SubObject
{
    public:
        SubObject(int i);
};

SubObject::SubObject(int i)
{
    throw std::runtime_error("Exception by SubObject ctor");
}

The MyClass class has a member variable of type int* and another one of type SubObject:

class MyClass
{
    public:
        MyClass();
    private:
        int* mData = nullptr;
        SubObject mSubObject;
};

The SubObject class does not have a default constructor. This means that you need to initialize mSubObject in the MyClass ctor-initializer. The constructor of MyClass uses a function-try-block to catch exceptions thrown in its ctor-initializer:

MyClass::MyClass() 
try
    : mData(new int[42]{ 1, 2, 3 }), mSubObject(42)
{
    /* ... constructor body ... */
}
catch (const std::exception& e)
{
    // Cleanup memory.
    delete[] mData;
    mData = nullptr;
    cout << "function-try-block caught: '" << e.what() << "'" << endl;
}

Remember that catch statements in a function-try-block for a constructor have to either rethrow the current exception, or throw a new exception. The preceding catch statement does not throw anything, so the C++ runtime automatically rethrows the current exception. Following is a simple function that uses the preceding class:

int main()
{
    try {
        MyClass m;
    } catch (const std::exception& e) {
        cout << "main() caught: '" << e.what() << "'" << endl;
    }
    return 0;
}

The output of the preceding example is as follows:

function-try-block caught: 'Exception by SubObject ctor'
main() caught: 'Exception by SubObject ctor'

Note that the code in the example can be dangerous. Depending on the order of initialization, it could be that mData contains garbage when entering the catch statement. Deleting such a garbage pointer causes undefined behavior. The solution in this example’s case is to use a smart pointer for the mData member, for example std::unique_ptr, and to remove the function-try-block. Therefore:

Function-try-blocks are not limited to constructors. They can be used with ordinary functions as well. However, for normal functions, there is no useful reason to use function-try-blocks because they can just as easily be converted to a simple try/catch block inside the function body. One notable difference when using a function-try-block on a normal function compared to a constructor is that rethrowing the current exception or throwing a new exception in the catch statements is not required and the C++ runtime will not automatically rethrow the exception.

Errors in Destructors

You should handle all error conditions that arise in destructors in the destructors themselves. You should not let any exceptions be thrown from destructors, for a couple of reasons:

  1. Destructors are implicitly marked as noexcept, unless they are marked with noexcept(false)2 or the class has any subobjects whose destructor is noexcept(false). If you throw an exception from a noexcept destructor, the C++ runtime calls std::terminate() to terminate the application.
  2. Destructors can run while there is another pending exception, in the process of stack unwinding. If you throw an exception from the destructor in the middle of stack unwinding, the C++ runtime calls std::terminate() to terminate the application. For the brave and curious, C++ does provide the ability to determine, in a destructor, whether you are executing as a result of a normal function exit or delete call, or because of stack unwinding. The function uncaught_exceptions(), declared in the <exception> header file, returns the number of uncaught exceptions, that is, exceptions that have been thrown but that have not reached a matching catch yet. If the result of uncaught_exceptions() is greater than zero, then you are in the middle of stack unwinding. However, correct use of this function is complicated, messy, and should be avoided. Note that before C++17, the function was called uncaught_exception() (singular), and returned a bool that was true if you were in the middle of stack unwinding.
  3. What action would clients take? Clients don’t call destructors explicitly: they call delete, which calls the destructor. If you throw an exception from the destructor, what is a client supposed to do? It can’t call delete on the object again, and it shouldn’t call the destructor explicitly. There is no reasonable action the client can take, so there is no reason to burden that code with exception handling.
  4. The destructor is your one chance to free memory and resources used in the object. If you waste your chance by exiting the function early due to an exception, you will never be able to go back and free the memory or resources.

PUTTING IT ALL TOGETHER

Now that you’ve learned about error handling and exceptions, let’s see it all coming together in a bigger example, a GameBoard class. This GameBoard class is based on the GameBoard class from Chapter 12. The implementation in Chapter 12 using a vector of vectors is the recommended implementation because even when an exception is thrown, the code is not leaking any memory due to the use of Standard Library containers. To be able to demonstrate handling memory allocation errors, the following version is adapted to use a raw pointer, GamePiece** mCells. First, here is the definition of the class without any exceptions:

class GamePiece {};

class GameBoard
{
    public:
        // general-purpose GameBoard allows user to specify its dimensions
        explicit GameBoard(size_t width = kDefaultWidth,
            size_t height = kDefaultHeight);
        GameBoard(const GameBoard& src); // Copy constructor
        virtual ~GameBoard();
        GameBoard& operator=(const GameBoard& rhs); // Assignment operator

        GamePiece& at(size_t x, size_t y);
        const GamePiece& at(size_t x, size_t y) const;

        size_t getHeight() const { return mHeight; }
        size_t getWidth() const { return mWidth; }

        static const size_t kDefaultWidth = 100;
        static const size_t kDefaultHeight = 100;

        friend void swap(GameBoard& first, GameBoard& second) noexcept;
    private:
        // Objects dynamically allocate space for the game pieces.
        GamePiece** mCells = nullptr;
        size_t mWidth = 0, mHeight = 0;
};

Note that you could also add move semantics to this GameBoard class. That would require adding a move constructor and move assignment operator, which both have to be noexcept. See Chapter 9 for details on move semantics.

Here are the implementations without any exceptions:

GameBoard::GameBoard(size_t width, size_t height)
    : mWidth(width), mHeight(height)
{
    mCells = new GamePiece*[mWidth];
    for (size_t i = 0; i < mWidth; i++) {
        mCells[i] = new GamePiece[mHeight];
    }
}

GameBoard::GameBoard(const GameBoard& src)
    : GameBoard(src.mWidth, src.mHeight)
{
    // The ctor-initializer of this constructor delegates first to the
    // non-copy constructor to allocate the proper amount of memory.

    // The next step is to copy the data.
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            mCells[i][j] = src.mCells[i][j];
        }
    }
}

GameBoard::~GameBoard()
{
    for (size_t i = 0; i < mWidth; i++) {
        delete[] mCells[i];
    }
    delete[] mCells;
    mCells = nullptr;
    mWidth = mHeight = 0;
}

void swap(GameBoard& first, GameBoard& second) noexcept
{
    using std::swap;

    swap(first.mWidth, second.mWidth);
    swap(first.mHeight, second.mHeight);
    swap(first.mCells, second.mCells);
}

GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
    // Check for self-assignment
    if (this == &rhs) {
        return *this;
    }

    // Copy-and-swap idiom
    GameBoard temp(rhs); // Do all the work in a temporary instance
    swap(*this, temp); // Commit the work with only non-throwing operations
    return *this;
}

const GamePiece& GameBoard::at(size_t x, size_t y) const
{
    return mCells[x][y];
}

GamePiece& GameBoard::at(size_t x, size_t y)
{
    return const_cast<GamePiece&>(std::as_const(*this).at(x, y));
}

Now, let’s retrofit the preceding class to include error handling and exceptions. The constructors and operator= can all throw bad_alloc because they perform memory allocation directly or indirectly. The destructor, cleanup(), getHeight(), getWidth(), and swap() throw no exceptions. The verifyCoordinate() and at() methods throw out_of_range if the caller supplies an invalid coordinate. Here is the retrofitted class definition:

class GamePiece {};

class GameBoard
{
    public:
        explicit GameBoard(size_t width = kDefaultWidth,
            size_t height = kDefaultHeight);
        GameBoard(const GameBoard& src);
        virtual ~GameBoard() noexcept;
        GameBoard& operator=(const GameBoard& rhs); // Assignment operator

        GamePiece& at(size_t x, size_t y);
        const GamePiece& at(size_t x, size_t y) const;

        size_t getHeight() const noexcept { return mHeight; }
        size_t getWidth() const noexcept { return mWidth; }

        static const size_t kDefaultWidth = 100;
        static const size_t kDefaultHeight = 100;

        friend void swap(GameBoard& first, GameBoard& second) noexcept;
    private:
        void cleanup() noexcept;
        void verifyCoordinate(size_t x, size_t y) const;

        GamePiece** mCells = nullptr;
        size_t mWidth = 0, mHeight = 0;
};

Here are the implementations with exception handling:

GameBoard::GameBoard(size_t width, size_t height)
{
    mCells = new GamePiece*[width] {};  // Array is zero-initialized!

    // Don't initialize the mWidth and mHeight members in the ctor-
    // initializer. These should only be initialized when the above
    // mCells allocation succeeds!
    mWidth = width;
    mHeight = height;

    try {
        for (size_t i = 0; i < mWidth; i++) {
            mCells[i] = new GamePiece[mHeight];
        }
    } catch (...) {
        cleanup();
        // Nest any caught exception inside a bad_alloc exception.
        std::throw_with_nested(std::bad_alloc());
    }
}

GameBoard::GameBoard(const GameBoard& src)
    : GameBoard(src.mWidth, src.mHeight)
{
    // The ctor-initializer of this constructor delegates first to the
    // non-copy constructor to allocate the proper amount of memory.

    // The next step is to copy the data.
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            mCells[i][j] = src.mCells[i][j];
        }
    }
}

GameBoard::~GameBoard() noexcept
{
    cleanup();
}

void GameBoard::cleanup() noexcept
{
    for (size_t i = 0; i < mWidth; i++)
        delete[] mCells[i];
    delete[] mCells;
    mCells = nullptr;
    mWidth = mHeight = 0;
}

void GameBoard::verifyCoordinate(size_t x, size_t y) const
{
    if (x >= mWidth)
        throw out_of_range("x-coordinate beyond width");
    if (y >= mHeight)
        throw out_of_range("y-coordinate beyond height");
}

void swap(GameBoard& first, GameBoard& second) noexcept
{
    using std::swap;

    swap(first.mWidth, second.mWidth);
    swap(first.mHeight, second.mHeight);
    swap(first.mCells, second.mCells);
}

GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
    // Check for self-assignment
    if (this == &rhs) {
        return *this;
    }

    // Copy-and-swap idiom
    GameBoard temp(rhs); // Do all the work in a temporary instance
    swap(*this, temp); // Commit the work with only non-throwing operations
    return *this;
}

const GamePiece& GameBoard::at(size_t x, size_t y) const
{
    verifyCoordinate(x, y);
    return mCells[x][y];
}

GamePiece& GameBoard::at(size_t x, size_t y)
{
    return const_cast<GamePiece&>(std::as_const(*this).at(x, y));
}

SUMMARY

This chapter described the issues related to error handling in C++ programs, and emphasized that you must design and code your programs with an error-handling plan. By reading this chapter, you learned the details of C++ exceptions syntax and behavior. The chapter also covered some of the areas in which error handling plays a large role, including I/O streams, memory allocation, constructors, and destructors. Finally, you saw an example of error handling in a GameBoard class.

NOTES

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset