© Ivor Horton and Peter Van Weert 2018
Ivor Horton and Peter Van WeertBeginning C++17https://doi.org/10.1007/978-1-4842-3366-5_15

15. Runtime Errors and Exceptions

Ivor Horton1  and Peter Van Weert2
(1)
Stratford-upon-Avon, Warwickshire, UK
(2)
Kessel-Lo, Belgium
 

Exceptions are used to signal errors or unexpected conditions in a program. While other error-handling mechanisms do exist, exceptions generally lead to simpler, cleaner code, in which you are less likely to miss an error. Particularly in combination with the RAII principle (short for “resource acquisition is initialization”), we will show that exceptions form the basis of some of the most effective programming patterns in modern C++.

In this chapter, you’ll learn:
  • What an exception is and when you should use exceptions

  • How you use exceptions to signal error conditions

  • How you handle exceptions in your code

  • What happens if you neglect to handle an exception

  • What RAII stands for and how this idiom facilitates writing exception-safe code

  • When to use the noexcept specifier

  • Why to be extra careful when using exceptions inside destructors

  • What types of exceptions are defined in the Standard Library

Handling Errors

Error handling is a fundamental element of successful programming. You need to equip your program to deal with potential errors and abnormal events, and this can often require more effort than writing the code that executes when things work the way they should. Every time your program accesses a file, a database, a network location, a printer, and so on, something unexpected could go wrong—a USB device is unplugged, a network connection is lost, a hardware error occurs, and so on. Even without external sources of errors, your code is likely not-bug free, and most nontrivial algorithms do fail at times for ambiguous or unexpected inputs. The quality of your error-handling code determines how robust your program is, and it is usually a major factor in making a program user-friendly. It also has a substantial impact on how easy it is to correct errors in the code or to add functionality to an application.

Not all errors are equal, and the nature of the error determines how best to deal with it. You don’t usually use exceptions for errors that occur in the normal use of a program. In many cases, you’ll deal with such errors directly where they occur. For example, when data is entered by the user, mistakes can result in erroneous input, but this isn’t really a serious problem. It’s usually quite easy to detect such errors, and the most appropriate course of action is often simply to discard the input and prompt the user to enter the data again. In this case, the error-handling code is integrated with the code that handles the overall input process. Generally, exceptions should be used if the function that notices the error cannot recover from this. A primary advantage of using exceptions to signal errors is that the error-handling code is separated completely from the code that caused the error.

Key is that the name exception is aptly chosen. Exceptions should be used only to signal exceptional conditions that you do not expect to occur in the normal course of events and that require special attention. You should definitely never use exceptions during the nominal execution of a program—for instance, to return a result from a function. Also, if a certain error occurs frequently and in most cases can be ignored, you could consider returning an error code of sorts instead of raising an exception, but only do that if you in fact want your program to regularly ignore an error condition. If the error needs further attention from the program or if the program should not continue after an error occurred, exceptions remain the recommended error-handling mechanism.

Understanding Exceptions

An exception is a temporary object, of any type, that is used to signal an error. An exception can in theory be of a fundamental type, such as int or const char*, but it’s usually and more appropriately an object of a class type. The purpose of an exception object is to carry information from the point at which the error occurred to the code that is to handle the error. In many situations, more than one piece of information is involved, so this is best done with an object of a class type.

When you recognize that something has gone wrong in the code, you can signal the error by throwing an exception. The term throwing effectively indicates what happens. The exception object is tossed to another block of code that catches the exception and deals with it. Code that may throw exceptions must be within a special block called a try block if an exception is to be caught. If a statement that is not within a try block throws an exception or a statement within a try block throws an exception that is not caught, the program terminates. We’ll discuss this further a little later in this chapter.

A try block is followed immediately by one or more catch blocks. Each catch block contains code to handle a particular kind of exception; for this reason, a catch block is sometimes referred to as an exception handler. All the code that deals with errors that cause exceptions to be thrown is within catch blocks that are completely separate from the code that is executed when everything works as it should.

Figure 15-1 shows a try block, which is a normal block between braces that is preceded by the try keyword. Each time the try block executes, it may throw any one of several different types of exception. Therefore, a try block can be followed by several catch blocks, each of which handles an exception of a different type. A catch block is a normal block between braces preceded by the catch keyword. The type of exception that a catch block deals with is identified by a single parameter between parentheses following the catch keyword.
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig1_HTML.gif
Figure 15-1.

A try block and its catch blocks

The code in a catch block executes only when an exception of a matching type is thrown. If a try block doesn’t throw an exception, then none of the catch blocks following the try block is executed. A try block always executes beginning with the first statement following the opening brace.

Throwing an Exception

It’s high time you threw an exception to find out what happens when you do. Although you should always use class objects for exceptions (as you’ll do later in the chapter), we’ll begin by using basic types because this will keep the code simple while we explain what’s going on. You throw an exception using a throw expression, which you write using the throw keyword. Here’s an example:

try
{
  // Code that may throw exceptions must be in a try block...
  if (test > 5)
    throw "test is greater than 5";       // Throws an exception of type const char*
  // This code only executes if the exception is not thrown...
}
catch (const char* message)
{
  // Code to handle the exception...
  // ...which executes if an exception of type 'char*' or 'const char*' is thrown
  std::cout << message << std::endl;
}

If the value of test is greater than 5, the throw statement throws an exception. In this case, the exception is the literal "test is greater than 5". Control is immediately transferred out of the try block to the first handler for the type of the exception that was thrown: const char*. There’s just one handler here, which happens to catch exceptions of type const char*, so the statement in the catch block executes, and this displays the exception.

Note

The compiler ignores the const keyword when matching the type of an exception that was thrown with the catch parameter type. We’ll examine this more thoroughly later.

Let’s try exceptions in a working example that will throw exceptions of type int and const char*. The output statements help you see the flow of control:

// Ex15_01.cpp
// Throwing and catching exceptions
#include <iostream>
int main()
{
  for (size_t i {}; i < 7; ++i)
  {
    try
    {
      if (i < 3)
        throw i;
      std::cout << "i not thrown - value is " << i << std::endl;
      if (i > 5)
        throw "Here is another!";
      std::cout << "End of the try block." << std::endl;
    }
    catch (size_t i)         // Catch exceptions of type size_t
    {
      std::cout << "i caught - value is " << i << std::endl;
    }
    catch (const char* message)    // Catch exceptions of type char*
    {
      std::cout << "message caught - value is \"" << message << '"' << std::endl;
    }
    std::cout << "End of the for loop body (after the catch blocks)"
              << " - i is " << i << std::endl;
  }
}

This example produces the following output:

i caught - value is 0
End of the for loop body (after the catch blocks) - i is 0
i caught - value is 1
End of the for loop body (after the catch blocks) - i is 1
i caught - value is 2
End of the for loop body (after the catch blocks) - i is 2
i not thrown - value is 3
End of the try block.
End of the for loop body (after the catch blocks) - i is 3
i not thrown - value is 4
End of the try block.
End of the for loop body (after the catch blocks) - i is 4
i not thrown - value is 5
End of the try block.
End of the for loop body (after the catch blocks) - i is 5
i not thrown - value is 6
message caught – value is "Here is another!"
End of the for loop body (after the catch blocks) - i is 6

The try block within the for loop contains code that will throw an exception of type size_t if i (the loop counter) is less than 3, and an exception of type const char* if i is greater than 5. Throwing an exception transfers control out of the try block immediately, so the output statement at the end of the try block executes only if no exception is thrown. The output shows that this is the case. You only get output from the last statement when i has the value 3, 4, or 5. For all other values of i, an exception is thrown, so the output statement is not executed.

The first catch block immediately follows the try block. All the exception handlers for a try block must immediately follow the try block. If you place any code between the try block and the first catch block or between successive catch blocks, the program won’t compile. The first catch block handles exceptions of type size_t, and you can see from the output that it executes when the first throw statement is executed. You can also see that the next catch block is not executed in this case. After this handler executes, control passes directly to the last statement at the end of the loop.

The second handler deals with exceptions of type char*. When the exception "Here is another!" is thrown, control passes from the throw statement directly to this handler, skipping the previous catch block. If no exception is thrown, neither of the catch blocks is executed. You could put this catch block before the previous handler, and the program would work just as well. On this occasion, the sequence of the handlers doesn’t matter, but that’s not always the case. You’ll see examples of when the order of the handlers is important later in this chapter.

The statement that identifies the end of a loop iteration in the output is executed whether a handler is executed. Throwing an exception that is caught doesn’t end the program—unless you want it to, of course—in which case you terminate the program in the catch block. If the problem that caused the exception can be fixed within the handler, then the program can continue.

The Exception-Handling Process

From the example, you should have a fairly clear idea of the sequence of events when an exception is thrown. Some other things happen in the background, though; you might be able to guess some of them if you think about how control is transferred from the try block to the catch block. The throw/ catch sequence of events is illustrated conceptually in Figure 15-2.
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig2_HTML.gif
Figure 15-2.

The mechanism behind throwing and catching an exception

Of course, a try block is a statement block, and you know that a statement block defines a scope. Throwing an exception leaves the try block immediately, so at that point all the automatic objects that have been defined within the try block prior to the exception being thrown are destroyed. The fact that none of the automatic objects created in the try block exists by the time the handler code is executed is most important; it implies that you must not throw an exception object that’s a pointer to an object that is local to the try block. It’s also the reason why the exception object is copied in the throw process.

Caution

An exception object must be of a type that can be copied. An object of a class type that has a private, protected, or deleted copy constructor can’t be used as an exception.

Because the throw expression is used to initialize a temporary object—and therefore creates a copy of the exception—you can throw objects that are local to the try block but not pointers to local objects. The copy of the object is used to initialize the parameter for the catch block that is selected to handle the exception.

A catch block is also a statement block, so when a catch block has finished executing, all automatic objects that are local to it (including the parameter) will be destroyed. Unless you transfer control out of the catch block using, for instance, a return statement, execution continues with the statement immediately following the last catch block for the try block. Once a handler has been selected for an exception and control has been passed to it, the exception is considered handled. This is true even if the catch block is empty and does nothing.

Code That Causes an Exception to Be Thrown

At the beginning of this discussion we said that try blocks enclose code that may throw an exception . However, this doesn’t mean that the code that throws an exception must be physically between the braces bounding the try block. It only needs to be logically within the try block. If a function is called within a try block, any exception that is thrown and not caught within that function can be caught by one of catch blocks for the try block. An example of this is illustrated in Figure 15-3. Two function calls are shown within the try block: fun1() and fun2(). Exceptions of type ExceptionType that are thrown within either function can be caught by the catch block following the try block. An exception that is thrown but not caught within a function may be passed on to the calling function the next level up. If it isn’t caught there, it can be passed one up to the next level; this is illustrated in Figure 15-3 by the exception thrown in fun3() when it is called by fun1(). There’s no try block in fun1(), so exceptions thrown by fun3() will be passed to the function that called fun1(). If an exception reaches a level where no further catch handler exists and it is still uncaught, then the program is typically terminated (we’ll discuss what happens after an uncaught exception in more detail later).
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig3_HTML.gif
Figure 15-3.

Exception thrown by functions called within a try block

Of course, if the same function is called from different points in a program, the exceptions that the code in the body of the function may throw can be handled by different catch blocks at different times. You can see an example of this situation in Figure 15-4.
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig4_HTML.gif
Figure 15-4.

Calling the same function from within different try blocks

As a consequence of the call in the first try block, the catch block for that try block handles any exceptions of type ExceptionType thrown by fun1(). When fun1() is called in the second try block, the catch handler for that try block deals with any exception of type ExceptionType that is thrown. From this you should be able to see that you can choose to handle exceptions at the level that is most convenient to your program structure and operation. In an extreme case, you could catch all the exceptions that arose anywhere in a program in main() just by enclosing the code in main() in a try block and appending a suitable variety of catch blocks.

Nested try Blocks

You can nest a try block inside another try block. Each try block has its own set of catch blocks to handle exceptions that may be thrown within it, and the catch blocks for a try block are invoked for exceptions thrown only within that try block. This process is shown in Figure 15-5.
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig5_HTML.gif
Figure 15-5.

Nested try blocks

Figure 15-5 shows one handler for each try block, but in general there may be several. The catch blocks catch exceptions of different types, but they could catch exceptions of the same type. When the code in an inner try block throws an exception, its handlers get the first chance to deal with it. Each handler for the try block is checked for a matching parameter type, and if none matches, the handlers for the outer try block have a chance to catch the exception. You can nest try blocks in this way to whatever depth is appropriate for your application.

When an exception is thrown by the code in the outer try block, that block’s catch handlers handle it, even if the statement originating the exception precedes the inner try block. The catch handlers for the inner try block can never be involved in dealing with exceptions thrown by code within the outer try block. The code within both try blocks may call functions, in which case the code within the body of the function is logically within the try block that called it. Any or all of the code within the body of the function could also be within its own try block, in which case this try block would be nested within the try block that called the function.

It all sounds rather complicated in words, but it’s much easier in practice. We’ll put together a simple example in which an exception is thrown and then see where it ends up. Once again, we’re going for explanation rather than gritty realism, so we’ll throw exceptions of type int and type long rather than objects of a class type. The code for this example demonstrates nested try blocks and throwing an exception within a function:

// Ex15_02.cpp
// Throwing exceptions in nested try blocks
#include <iostream>
void throwIt(int i)
{
  throw i;                             // Throws the parameter value
}
int main()
{
  for (int i {}; i <= 5; ++i)
  {
    try
    {
      std::cout << "outer try:\n";
      if (i == 0)
        throw i;                       // Throw int exception
      if (i == 1)
        throwIt(i);                    // Call the function that throws int
      try
      {                                // Nested try block
        std::cout << "inner try:\n";
        if (i == 2)
          throw static_cast<long>(i);  // Throw long exception
        if (i == 3)
          throwIt(i);                  // Call the function that throws int
      }                                // End nested try block
      catch (int n)
      {
        std::cout << "Catch int for inner try. " << "Exception " << n << std::endl;
      }
      std::cout << "outer try:\n";
      if (i == 4)
        throw i;                       // Throw int
      throwIt(i);                      // Call the function that throws int
    }
    catch (int n)
    {
      std::cout << "Catch int for outer try. " << "Exception " << n << std::endl;
    }
    catch (long n)
    {
      std::cout << "Catch long for outer try. " << "Exception " << n << std::endl;
    }
  }
}

This produces the following output:

outer try:
Catch int for outer try. Exception 0
outer try:
Catch int for outer try. Exception 1
outer try:
inner try:
Catch long for outer try. Exception 2
outer try:
inner try:
Catch int for inner try. Exception 3
outer try:
Catch int for outer try. Exception 3
outer try:
inner try:
outer try:
Catch int for outer try. Exception 4
outer try:
inner try:
outer try:
Catch int for outer try. Exception 5

How It Works

The throwIt() function throws its parameter value. If you were to call this function from outside a try block, it would immediately cause the program to end (as explained in detail later). All the exceptions are thrown within the for loop. Within the loop, you determine when to throw an exception and what kind of exception to throw by testing the value of the loop variable, i, in successive if statements. At least one exception is thrown on each iteration. Entry to each try block is recorded in the output, and because each exception has a unique value, you can easily see where each exception is thrown and caught.

The first exception is thrown from the outer try block when the loop variable, i, is 0. You can see from the output that the catch block for exceptions of type int that follows the outer try block catches this exception. The catch block for the inner try block has no relevance because it can only catch exceptions thrown in the inner try block.

The next exception is thrown in the outer try block when i is 1 as a result of calling throwIt(). This is also caught by the catch block for int exceptions that follows the outer try block. The next two exceptions, however, are thrown in the inner try block. The first is an exception of type long. No catch block for the inner try block for this type of exception exists, so it propagates to the outer try block. Here, the catch block for type long handles it, as you can see from the output. The second exception is of type int and is thrown in the body of the throwIt() function. No try block exists in this function, so the exception propagates to the point where the function was called in the inner try block. The exception is then caught by the catch block for exceptions of type int that follows the inner try block.

When a handler for the inner try block catches an exception, execution continues with the remainder of the outer try block. Thus, when i is 3, you get output from the catch block for the inner try block, plus output from the handler for int exceptions for the outer try block. The latter exception is thrown as a result of the throwIt() function call at the end of the outer try block. Finally, two more exceptions are thrown in the outer try block. The handler for int exceptions for the outer try block catches both. The second exception is thrown within the body of throwIt(), and because it is called in the outer try block, the catch block following the outer try block handles it.

Even though these examples clearly show the mechanics of throwing and catching exceptions and what happens with nested try blocks, the exceptions they use are not particularly realistic. Exceptions in real programs are invariably class objects. Let’s therefore move on to take a closer look at exceptions that are objects.

Class Objects as Exceptions

You can throw any type of class object as an exception. However, keep in mind that the idea of an exception object is to communicate information to the handler about what went wrong. Therefore, it’s usually appropriate to define a specific exception class that is designed to represent a particular kind of problem. This is likely to be application-specific, but your exception class objects almost invariably contain a message of some kind explaining the problem and possibly some sort of error code. You can also arrange for an exception object to provide additional information about the source of the error in whatever form is appropriate.

Let’s define a simple exception class. We’ll do so in a header file with a fairly generic name, MyTroubles.h, because we’ll be adding to this file later:

// MyTroubles.h Exception class definition
#ifndef MYTROUBLES_H
#define MYTROUBLES_H
#include <string>
#include <string_view>
class Trouble
{
private:
  std::string message;
public:
  Trouble(std::string_view str = "There's a problem") : message {str} {}
  std::string_view what() const { return message; }
};
#endif

Objects of the Trouble class simply store a message indicating a problem and are thus ideally suited as simple exception objects. A default message is defined in the parameter list for the constructor, so you can use the default constructor to get an object that contains the default message. Whether you should be using such default messages is another matter entirely, of course. Remember, the idea is usually to provide information regarding the cause of the problem to aid with problem diagnosis. The what() member function returns the current message. To keep the logic of exception handling manageable, your functions should ensure that member functions of an exception class don’t throw exceptions. Later in this chapter, you’ll see how you can explicitly signal that a member function will never throw exceptions.

Let’s find out what happens when a class object is thrown by throwing a few. As in the previous examples, we won’t bother to create errors. We’ll just throw exception objects so that you can follow what happens to them under various circumstances. We’ll exercise the exception class with a simple example that throws some exception objects in a loop:

// Ex15_03.cpp
// Throw an exception object
#include <iostream>
#include "MyTroubles.h"
void trySomething(int i);
int main()
{
  for (int i {}; i < 2; ++i)
  {
    try
    {
      trySomething(i);
    }
    catch (const Trouble& t)
    {
      // What seems to be the trouble?
      std::cout << "Exception: " << t.what() << std::endl;
    }
  }
}
void trySomething(int i)
{
  // There's always trouble when ‘trying something’ here...
  if (i == 0)
    throw Trouble {};
  else
    throw Trouble {"Nobody knows the trouble I've seen..."};
}

This produces the following output:

Exception: There's a problem
Exception: Nobody knows the trouble I've seen...

Two exception objects are thrown by trySomething() during the for loop. The first is created by the default constructor for the Trouble class and therefore contains the default message string. The second exception object is thrown in the else clause of the if statement and contains a message that is passed as the argument to the constructor. The catch block catches both exception objects.

The parameter for the catch block is a reference. Remember that an exception object is always copied when it is thrown, so if you don’t specify the parameter for a catch block as a reference, it’ll be copied a second time—quite unnecessarily. The sequence of events when an exception object is thrown is that first the object is copied to create a temporary object and the original is destroyed because the try block is exited and the object goes out of scope. The copy is passed to the catch handler—by reference if the parameter is a reference. If you want to observe these events taking place, just add a copy constructor and a destructor containing some output statements to the Trouble class.

Matching a Catch Handler to an Exception

We said earlier that the handlers following a try block are examined in the sequence in which they appear in the code, and the first handler whose parameter type matches the type of the exception will be executed. With exceptions that are basic types (rather than class types), an exact type match with the parameter in the catch block is necessary. With exceptions that are class objects, implicit conversions may be applied to match the exception type with the parameter type of a handler. When the parameter type is being matched to the type of the exception that was thrown, the following are considered to be a match:
  • The parameter type is the same as the exception type, ignoring const.

  • The type of the parameter is a direct or indirect base class of the exception class type, or a reference to a direct or indirect base class of the exception class, ignoring const.

  • The exception and the parameter are pointers, and the exception type can be converted implicitly to the parameter type, ignoring const.

The possible type conversions listed here have implications for how you sequence the catch blocks for a try block. If you have several handlers for exception types within the same class hierarchy, then the most derived class type must appear first and the most base class type last. If a handler for a base type appears before a handler for a type derived from that base, then the base type is always selected to handle the derived class exceptions. In other words, the handler for the derived type is never executed.

Let’s add a couple more exception classes to the header containing the Trouble class and use Trouble as a base class for them. Here’s how the contents of the header file MyTroubles.h will look with the extra classes defined:

// MyTroubles.h Exception classes
#ifndef MYTROUBLES_H
#define MYTROUBLES_H
#include <string>
#include <string_view>
class Trouble
{
private:
  std::string message;
public:
  Trouble(std::string_view str = "There's a problem") : message {str} {}
  virtual ∼Trouble() = default;     // Base classes must always have a virtual destructor!
  virtual std::string_view what() const { return message; }
};
// Derived exception class
class MoreTrouble : public Trouble
{
public:
  MoreTrouble(std::string_view str = "There's more trouble...") : Trouble {str} {}
};
// Derived exception class
class BigTrouble : public MoreTrouble
{
public:
  BigTrouble(std::string_view str = "Really big trouble...") : MoreTrouble {str} {}
};
#endif

Note that the what() member and the destructor of the base class have been declared as virtual. Therefore, the what() function is also virtual in the classes derived from Trouble. It doesn’t make much of a difference here, but it would in principle allow derived classes to redefine what(). Remembering to declare a virtual destructor in a base class is important, though. Other than different default strings for the message, the derived classes don’t add anything to the base class. Often, just having a different class name can differentiate one kind of problem from another. You just throw an exception of a particular type when that kind of problem arises; the internals of the classes don’t have to be different. Using a different catch block to catch each class type provides the means to distinguish different problems. Here’s the code to throw exceptions of the Trouble, MoreTrouble, and BigTrouble types, as well as the handlers to catch them:

// Ex15_04.cpp
// Throwing and catching objects in a hierarchy
#include <iostream>
#include "MyTroubles.h"
int main()
{
  Trouble trouble;
  MoreTrouble moreTrouble;
  BigTrouble bigTrouble;
  for (int i {}; i < 7; ++i)
  {
    try
    {
      if (i == 3)
        throw trouble;
      else if (i == 5)
        throw moreTrouble;
      else if (i == 6)
        throw bigTrouble;
    }
    catch (const BigTrouble& t)
    {
      std::cout << "BigTrouble object caught: " << t.what() << std::endl;
    }
    catch (const MoreTrouble& t)
    {
      std::cout << "MoreTrouble object caught: " << t.what() << std::endl;
    }
    catch (const Trouble& t)
    {
      std::cout << "Trouble object caught: " << t.what() << std::endl;
    }
    std::cout << "End of the for loop (after the catch blocks) - i is " << i << std::endl;
  }
}

Here’s the output:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
MoreTrouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
BigTrouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6

How It Works

After creating one object of each class type, the for loop throws one of them as an exception. Which object is selected depends on the value of the loop variable, i. Each of the catch blocks contains a different message, so the output shows which catch handler is selected when an exception is thrown. In the handlers for the two derived types, the inherited what() function still returns the message. Note that the parameter type for each of the catch blocks is a reference, as in the previous example. One reason for using a reference is to avoid making another copy of the exception object. In the next example, you’ll see another, more crucial reason why you should always use a reference parameter in a handler.

Each handler displays the message contained in the object thrown, and you can see from the output that each handler is called to correspond with the type of the exception thrown. The ordering of the handlers is important because of the way the exception is matched to a handler and because the types of your exception classes are related. Let’s explore that in a little more depth.

Catching Derived Class Exceptions with a Base Class Handler

Exceptions of derived class types are implicitly converted to a base class type for the purpose of matching a catch block parameter, so you could catch all the exceptions thrown in the previous example with a single handler. You can modify the previous example to see this happening. Just delete or comment out the two derived class handlers from main() in the previous example:

// Ex15_05.cpp
// Catching exceptions with a base class handler
#include <iostream>
#include "MyTroubles.h"
int main()
{
  Trouble trouble;
  MoreTrouble moreTrouble;
  BigTrouble bigTrouble;
  for (int i {}; i < 7; ++i)
  {
    try
    {
      if (i == 3)
        throw trouble;
      else if (i == 5)
        throw moreTrouble;
      else if (i == 6)
        throw bigTrouble;
    }
    catch (const Trouble& t)
    {
      std::cout << "Trouble object caught: " << t.what() << std::endl;
    }
    std::cout << "End of the for loop (after the catch blocks) - i is " << i << std::endl;
  }
}

The program now produces this output:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
Trouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
Trouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6

The catch block with the parameter of type const Trouble& now catches all the exceptions thrown in the try block. If the parameter in a catch block is a reference to a base class, then it matches any derived class exception. So, although the output proclaims “Trouble object caught” for each exception, the output actually corresponds to objects of other classes that are derived from Trouble.

The dynamic type is retained when the exception is passed by reference. To verify this is indeed so, you could obtain the dynamic type and display it using the typeid() operator. Just modify the code for the handler to the following:

catch (const Trouble& t)
{
  std::cout << typeid(t).name() << " object caught: " << t.what() << std::endl;
}

Remember, the typeid() operator returns an object of the type_info class, and calling its name() member returns the class name. With this modification to the code, the output shows that the derived class exceptions still retain their dynamic types, even though the reference in the exception handler is to the base class. For the record, the output from this version of the program looks like this:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
class Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
class MoreTrouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
class BigTrouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6

Note

If your compiler’s version of the typeid() operator results in mangled names, the names of the exception classes in your program’s output may look less pleasing to the eye. This was discussed in Chapter 14. For illustration’s sake, we will always show the results in an unmangled, human-readable format.

Try changing the parameter type for the handler to Trouble so that the exception is caught by value rather than by reference:

catch (Trouble t)
{
  std::cout << typeid(t).name() << " object caught: " << t.what() << std::endl;
}

This version of the program produces the output:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
class Trouble object caught: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
class Trouble object caught: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
class Trouble object caught: Really big trouble...
End of the for loop (after the catch blocks) - i is 6

Here, the Trouble handler is still selected for the derived class objects, but the dynamic type is not preserved. This is because the parameter is initialized using the base class copy constructor, so any properties associated with the derived class are lost. Only the base class subobject of the original derived class object is retained. This is an example of object slicing, which occurs because the base class copy constructor knows nothing about derived objects. As explained in Chapter 13, object slicing is a common source of error caused by passing objects by value. This leads us to the inevitable conclusion that you should always use reference parameters in catch blocks:

Tip

The golden rule for exceptions is to always throw by value and catch by reference (reference-to-const, normally). In other words, you mustn’t throw a new’ed exception (and definitely no pointer to a local object), nor should you ever catch an exception object by value. Obviously, catching by value would result in a redundant copy, but that’s not the worst of it. Catching by value may slice off parts of the exception object! The reason this is so important is that exception slicing might just slice off precisely that valuable piece of information that you need to diagnose which error occurred and why!

Rethrowing Exceptions

When a handler catches an exception, it can rethrow it to allow a handler for an outer try block to catch it. You rethrow the current exception with a statement consisting of just the throw keyword:

throw;                           // Rethrow the exception

This rethrows the existing exception object without copying it. You might rethrow an exception if a handler that catches exceptions of more than one derived class type discovers that the type of the exception requires it to be passed on to another level of try block. You might want to register or log the point in the program where an exception was thrown, before rethrowing it. Or you may need to clean up some resources—release some memory, close a database connection, etc.—before rethrowing the exception for handling in a caller function.

Note that rethrowing an exception from the inner try block doesn’t make the exception available to other handlers for the inner try block. When a handler is executing, any exception that is thrown (including the current exception) needs to be caught by a handler for a try block that encloses the current handler, as illustrated in Figure 15-6. The fact that a rethrown exception is not copied is important, especially when the exception is a derived class object that initialized a base class reference parameter. We’ll demonstrate this with an example.
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig6_HTML.gif
Figure 15-6.

Rethrowing an exception

This example throws Trouble, MoreTrouble, and BigTrouble exception objects and then rethrows some of them to show how the mechanism works:

// Ex15_06.cpp
// Rethrowing exceptions
#include <iostream>
#include "MyTroubles.h"
int main()
{
  Trouble trouble;
  MoreTrouble moreTrouble;
  BigTrouble bigTrouble;
  for (int i {}; i < 7; ++i)
  {
    try
    {
      try
      {
        if (i == 3)
          throw trouble;
        else if (i == 5)
          throw moreTrouble;
        else if (i == 6)
          throw bigTrouble;
      }
      catch (const Trouble& t)
      {
        if (typeid(t) == typeid(Trouble))
          std::cout << "Trouble object caught in inner block: " << t.what() << std::endl;
        else
          throw;                            // Rethrow current exception
      }
    }
    catch (const Trouble& t)
    {
      std::cout << typeid(t).name() << " object caught in outer block: "
                << t.what() << std::endl;
    }
    std::cout << "End of the for loop (after the catch blocks) - i is " << i << std::endl;
  }
}

This example displays the following output:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
Trouble object caught in inner block: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
class MoreTrouble object caught in outer block: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
class BigTrouble object caught in outer block: Really big trouble...
End of the for loop (after the catch blocks) - i is 6

The for loop works as in the previous example, but this time there is one try block nested inside another. The same sequence of exception objects as the previous example objects is thrown in the inner try block, and all the exception objects are caught by the matching catch block because the parameter is a reference to the base class, Trouble. The if statement in the catch block tests the class type of the object passed and executes the output statement if it is of type Trouble. For any other type of exception, the exception is rethrown and therefore available to be caught by the catch block for the outer try block. The parameter is also a reference to Trouble so it catches all the derived class objects. The output shows that it catches the rethrown objects and they’re still in pristine condition.

You might imagine that the throw statement in the handler for the inner try block is equivalent to the following statement:

throw t;                                    // Rethrow current exception

After all, you’re just rethrowing the exception, aren’t you? The answer is no; there’s a crucial difference. If you make this modification to the program code and run it again, you’ll get this output:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
Trouble object caught in inner block: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
class Trouble object caught in outer block: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
class Trouble object caught in outer block: Really big trouble...
End of the for loop (after the catch blocks) - i is 6

The statement with an explicit exception object specified is throwing a new exception, not rethrowing the original one. This results in the orginal exception object being copied, using the copy constructor for the Trouble class. It’s that vexing object slicing problem again! The derived portion of each object is sliced off, so you are left with just the base class subobject in each case. You can see from the output that the typeid() operator identifies all the exceptions as type Trouble. Therefore:

Tip

Always throw by value, catch by reference, and rethrow using a throw; statement.

Unhandled Exceptions

If a thrown exception is not caught, either directly or indirectly (that is, it does not have to be caught within the same function, as we discussed earlier), then the program is terminated instantly. And you really should expect such a termination to be fairly abrupt. Destructors for static objects will not get called anymore, for one; there is even no guarantee that the destructors of any objects still allocated on the call stack will be executed. In other words, the program essentially instantly crashes.

Note

In actuality, if an exception goes uncaught, the Standard Library function std::terminate() is called (declared in the exception header), which in turn by default calls std::abort() (declared in cstdlib), which then terminates the program. This sequence of events for an uncaught exception is shown in Figure 15-7.

It is technically possible to override the behavior of std::terminate() by passing a function pointer to std::set_terminate(). However, this is rarely recommended and should be reserved for exceptional (pun intended…) cases. There is also not that much you are allowed to do in a terminate handler. Acceptable uses include making sure that certain critical resources are properly cleaned up or writing a so-called crash dump that your customers can then send to you for further diagnosis. These topics are too advanced for this book to warrant further discussion. One thing you must never do is attempt to keep your program running after std::terminate() is called! std::terminate(), by definition, is called after an irrecoverable error; any attempt to recover results in undefined behavior. Your terminate handler must always end with one of two function calls: either std::abort() or std::_Exit().1 Both functions end the program without performing any further cleanup (the difference between the two is that with the latter you can decide which error code the process returns to the environment).

../images/326945_5_En_15_Chapter/326945_5_En_15_Fig7_HTML.gif
Figure 15-7.

Uncaught exceptions

Terminating the program after an uncaught exception may sound harsh, but what other alternative is there really? Remember, you normally throw an exception if something unexpected and unrecoverable has happened that needs further attention. If it then turns out that there is no code in place to appropriately deal with this error, what other course of action is there? The program is inherently in some unexpected, erroneous state, so carrying on as if nothing had happened would generally just lead to more, and potentially far worse, errors. Such secondary errors may furthermore be much harder to diagnose. The only sensible course of action is therefore to halt the execution.

Catching All Exceptions

You can use an ellipsis (...) as the parameter specification for a catch block to indicate that the block should handle any exception:

try
{
  // Code that may throw exceptions...
}
catch (...)
{
  // Code to handle any exception...
}

This catch block handles an exception of any type, so a handler like this must always be last in the sequence of handlers for a try block. Of course, you have no idea what the exception is, but at least you can prevent your program from terminating because of an uncaught exception. Note that even though you don’t know anything about it, you can still rethrow the exception as you did in the previous example.

You can modify the previous example to catch all the exceptions for the inner try block by using an ellipsis in place of the parameter:

// Ex15_07.cpp
// Catching any exception
#include <iostream>
#include <typeinfo> // For use of typeid()
#include "MyTroubles.h"
int main()
{
  Trouble trouble;
  MoreTrouble moreTrouble;
  BigTrouble bigTrouble;
  for (int i {}; i < 7; ++i)
  {
    try
    {
      try
      {
        if (i == 3)
          throw trouble;
        else if (i == 5)
          throw moreTrouble;
        else if (i == 6)
          throw bigTrouble;
      }
      catch (const BigTrouble& bt)
      {
        std::cout << "Oh dear, big trouble. Let's handle it here and now." << std::endl;
        // Do not rethrow...
      }
      catch (...)       // Catch any other exception
      {
        std::cout << "We caught something else! Let's rethrow it. " << std::endl;
        throw;          // Rethrow current exception
      }
    }
    catch (const Trouble& t)
    {
      std::cout << typeid(t).name() << " object caught in outer block: "
                << t.what() << std::endl;
    }
    std::cout << "End of the for loop (after the catch blocks) - i is " << i << std::endl;
  }
}

This produces the following output:

End of the for loop (after the catch blocks) - i is 0
End of the for loop (after the catch blocks) - i is 1
End of the for loop (after the catch blocks) - i is 2
We caught something else! Let's rethrow it.
class Trouble object caught in outer block: There's a problem
End of the for loop (after the catch blocks) - i is 3
End of the for loop (after the catch blocks) - i is 4
We caught something else! Let's rethrow it.
class MoreTrouble object caught in outer block: There's more trouble...
End of the for loop (after the catch blocks) - i is 5
Oh dear, big trouble. Let's handle that here and now.
End of the for loop (after the catch blocks) - i is 6

The last catch block for the inner try block has an ellipsis as the parameter specification, so any exception that is thrown but not caught by any of the other catch blocks of the same try block will be caught by this catch block. Every time an exception is caught there, a message displays, and the exception is rethrown to be caught by the catch block for the outer try block. There, its type is properly identified, and the string returned by its what() member is displayed. Exceptions of class BigTrouble will be handled by the corresponding inner catch block; as they are not rethrown there, they do not reach the outer catch block.

Caution

If your code will throw exceptions of different types, it may be tempting to use a catch-all block to catch them all at once. After all, it’s less work than enumerating catch blocks for every possible exception type. Similarly, if you’re calling functions that you’re less familiar with, quickly adding a catch-all block is much easier than researching which exception types these functions may throw. This is rarely the best approach, though. A catch-all block risks catching exceptions that need more specific handling or sweeping unexpected, dangerous errors under the rug. They also do not allow for much error logging or diagnosis. Still, all too often we encounter patterns such as this in code:

try
{
   DoSomething();
}
catch (...)
{
  // Oops. Something went wrong... Let's ignore it and cross our fingers...
}

The comments usually don’t actually contain the part about ignoring the error and crossing your fingers, but they might as well. The motivation behind such patterns is generally “Anything is better than uncaught exceptions.” The truth of the matter is that this is mostly just a symptom of a lazy programmer. There is no substitute for proper, thought-through error-handling code when it comes to delivering stable, fault-tolerant programs. And like we said in the introduction of this chapter, writing error handling and recovery code will take time. So while catch-all blocks may be tempting shortcuts, it’s usually preferred to explicitly check which exception types may be raised by the functions that you call and for each consider whether you should either add a catch block and/or leave it to a calling function to handle the exception. Once you know the exception type, you can usually extract more useful information from the object (like with the what() function of our Trouble class) and use this for proper error handling and logging. Note also that, especially during development, a program crash is often preferable over a catch-all. Then at least you learn about potential errors, instead of blindly ignoring them, and can adjust your code to properly prevent them or recover from them. Make no mistake, we do not mean to say that catch-all blocks should never be used—they certainly have their uses. Catch-alls that rethrow after some logging or cleanup, for instance, can be particularly useful. We just want to caution you against using them as an easy, subpar substitute for more targeted error handling.

Functions That Don’t Throw Exceptions

In principle, any function can throw an exception, including regular functions, member functions, virtual functions, overloaded operators, and even constructors and destructors. So, each time you call a function somewhere, anywhere, you should learn to think about the potential exceptions that might come out and whether you should handle them with a try block. Of course, this does not mean every function call needs to be surrounded by a try block. As long as you are not working in main(), it is often perfectly acceptable to delegate the responsibility of catching the exception to the callees of the function.

Sometimes, though, you know full well that the function you are writing won’t ever throw an exception. Some types of functions should even never throw an exception at all. This section briefly discusses these situations and the language facilities C++ provides for them.

The noexcept Specifier

By appending the noexcept keyword to the function header, you specify that a function will never throw exceptions. For instance, the following specifies that the doThat() functions will never throw:

void doThat(int argument) noexcept;

If you see a noexcept in a function’s header, you can be sure that this function will never throw an exception. The compiler will make sure of it. If a noexcept function does unwittingly throw an exception and that exception is not caught within the function, the exception will not be propagated to the calling function. Instead, the C++ program will treat this as an irrecoverable error and call std::terminate(). As discussed earlier in this chapter, std::terminate() always results in an abrupt termination of the process.

Note that this does not mean that no exceptions are allowed to be thrown within the function itself; it only means that no exception will ever escape the function. That is, if an exception is thrown during the execution of a noexcept function, it must be caught somewhere within that function and not rethrown. For example, an implementation of doThat() of the following form would be perfectly legal:

void doThat(int argument) noexcept
{
  try
  {
    // Code for the function...
  }
  catch (...)
  {
    // Handles all exceptions and does not rethrow...
  }
}

The noexcept specifier may be used on all functions, including member functions, constructors, and destructors. You will encounter concrete examples later in this chapter.

In later chapters, you’ll even encounter several types of functions that should always be declared as noexcept. This includes swap() functions (discussed in Chapter 16) and move members (discussed in Chapter 17).

Note

Prior to the C++17 language standard, you could specify a list of exception types a function could throw by appending throw(type 1 , ..., type N ) to a function header. Because such specifications were not effective, they are now no longer supported. The C++17 standard only retains throw() (with an empty list) as a deprecated synonym for noexcept.

Exceptions and Destructors

Starting with C++11, destructors are mostly implicitly noexcept. Even if you define a destructor without a noexcept specification, the compiler will normally add one implicitly. This means that should the destructor of the following class be executed, the exception will never leave the destructor. Instead, std::terminate() shall always be called (in accordance with the implicit noexcept specifier added by the compiler):

class MyClass
{
public:
  ∼MyClass() { throw std::exception{}; }
};

It is in principle possible to define a destructor from which exceptions may be thrown. You could do so by adding an explicit noexcept(false) specification. But since you should normally never do this,2 we won’t discuss or consider this possibility any further in this book.

Tip

Never allow an exception to leave a destructor. All destructors are normally3 noexcept, even if not specified as such explicitly, so any exception they throw will trigger a call to std::terminate().

Exceptions and Resource Leaks

Making sure all exceptions are caught prevents catastrophic program failure. And catching them with properly positioned and sufficiently fine-grained catch blocks allows you to properly handle all errors. The result is a program that, at all times, either presents the desired result or can inform the user precisely what went wrong. But this is not the end of the story! A program that appears to be functioning robustly from the outside may still contain hidden defects. Consider, for instance, the following example program (it uses the same MyTroubles.h as Ex15_07):

// Ex15_08.cpp
// Exceptions may result in resource leaks!
#include <iostream>
#include <cmath>                          // For std::sqrt()
#include "MyTroubles.h"
double DoComputeValue(double);            // A function to compute a single value
double* ComputeValues(size_t howMany);    // A function to compute an array of values
int main()
{
  try
  {
    double* values = ComputeValues(10000);
    // unfortunately, we won't be making it this far...
    delete[] values;
  }
  catch (const Trouble&)
  {
    std::cout << "No worries: I've caught it!" << std::endl;
  }
}
double* ComputeValues(size_t howMany)
{
  double* values = new double[howMany];
  for (size_t i = 0; i < howMany; ++i)
    values[i] = DoComputeValue(i);
  return values;
}
double DoComputeValue(double value)
{
  if (value < 100)
    return std::sqrt(value);   // Return the square root of the input value
  else
    throw Trouble{"The trouble with trouble is, it starts out as fun!"};
}

If you run this program, a Trouble exception is thrown as soon as the loop counter in ComputeValues() reaches 100. Because the exception is caught in main(), the program does not crash. It even reassures the user that all is well. If this was a real program, you could even inform the user about what exactly went wrong with this operation and allow the user to continue. But that does not mean you are out of the woods! Can you spot what else has gone wrong with this program?

The ComputeValues() function allocates an array of double values in the free store, attempts to fill them, and then returns the array to its caller. It is the responsibility of the caller—in this case main()—to deallocate this memory. However, because an exception is thrown halfway through the execution of ComputeValues(), its values array is never actually returned to main(). Therefore, the array is never deallocated either. In other words, we have just leaked an array of 10,000 doubles!

Assuming DoComputeValue() inherently leads to the occasional Trouble exception, the only place where we can fix this leak is in the ComputeValues() function. After all, main() never even receives a pointer to the leaked memory, so there is little that can be done about it there. In the spirit of this chapter thus far, a first obvious solution would therefore be to add a try block to ComputeValues() as follows (you can find this solution in Ex15_08A):

double* ComputeValues(size_t howMany)
{
  double* values = new double[howMany];
  try
  {
    for (size_t i = 0; i < howMany; ++i)
      values[i] = DoComputeValue(i);
    return values;
  }
  catch (const Trouble&)
  {
    std::cout << "I sense trouble... Freeing memory..." << std::endl;
    delete[] values;
    throw;
  }
}

Notice that values has to be defined outside the try block. Otherwise, the variable would be local to the try block, and we could not refer to it anymore from within the catch block. If you redefine ComputeValues() like this, no further changes are required to the rest of the program. It will also still have a similar outcome, except that this time the values array is not leaked:

I sense trouble... Freeing memory...
No worries: I've caught it!

While this ComputeValues() function with the try block makes for a perfectly correct program, it is not the most recommended approach. The function’s code has become about twice as long and about twice as complicated as well. The next section introduces better solutions that do not suffer these shortcomings.

Resource Acquisition Is Initialization

One of the hallmarks of modern C++ is the so-called RAII idiom, short for “resource acquisition is initialization.” Its premise is that each time you acquire a resource you should do so by initializing an object. Memory in the free store is a resource, but other examples include file handles (while holding these, other processes often may not access a file), mutexes (used for thread synchronization, as we’ll discuss in a later chapter), network connections, and so on. As per RAII, every such resource should be managed by an object, either allocated on the stack or as a member variable. The trick to avoid resource leaks is then that, by default, the destructor of that object makes sure the resource is always freed.

Let’s create a simple RAII class to demonstrate how this idiom works:

class DoubleArrayRAII final
{
private:
  double* resource;
public:
  DoubleArrayRAII(size_t size) : resource{ new double[size] } {}
  ∼DoubleArrayRAII()
  {
    std::cout << "Freeing memory..." << std::endl;
    delete[] resource;
  }
  // Delete copy constructor and assignment operator
  DoubleArrayRAII(const DoubleArrayRAII&) = delete;
  DoubleArrayRAII& operator=(const DoubleArrayRAII&) = delete;
  // Array subscript operator
  double& operator[](size_t index) noexcept { return resource[index]; }
  const double& operator[](size_t index) const noexcept { return resource[index]; }
  // Function to access the encapsulated resource
  double* get() const noexcept { return resource; }
  // Function to instruct the RAII object to hand over the resource.
  // Once called, the RAII object shall no longer release the resource
  // upon destruction anymore. Returns the resource in the process.
  double* release() noexcept
  {
    double* result = resource;
    resource = nullptr;
    return result;
  }
};

The resource—in this case the memory to hold a double array —is acquired by the constructor of the RAII object and released by its destructor. For this RAII class, it’s critical that the resource, the memory allocated for its array, is released only once. This implies that we mustn’t allow copies to be made of an existing DoubleArrayRAII; otherwise, we end up having two DoubleArrayRAII objects pointing to the same resource. You accomplish this by deleting both copy members (as shown in Chapter 12).

An RAII object often mimics the resource it manages by adding the appropriate member functions and operators. In our case, the resource is an array, so we define the familiar array subscript operators. Besides these, additional functions typically exist to access the resource itself (our get() function) and often also to release the RAII object of its responsibility of freeing the resource (the release() function).

With the help of this RAII class, you can safely rewrite the ComputeValues() function as follows:

double* ComputeValues(size_t howMany)
{
  DoubleArrayRAII values{howMany};
  for (size_t i = 0; i < howMany; ++i)
      values[i] = DoComputeValue(i);
  return values.release();
}

If anything goes wrong now during the computation of the values (that is, if an exception is thrown by DoComputeValue(i)), the compiler guarantees that the destructor of the RAII object is called, which in turn guarantees that the memory of the double array is properly released. Once all values have been computed, we again hand over the double array to the caller as before, along with the responsibility of deleting it. Notice that if we hadn’t called release() prior to returning, the destructor of the DoubleArrayRAII object would still be deleting the array.

Caution

The destructor of the DoubleArrayRAII object is called regardless of whether an exception occurs or not. Therefore, if we were calling get() instead of release() at the last line of the ComputeValues() function, we would still be deleting the array that is being returned from the function. In other applications of the RAII idiom, the idea is to always release the acquired resources, even in case of success. For instance, when performing file input/output (I/O), you normally want to release the file handle at the end, irrespective of whether the I/O operations succeeded or failed.

The resulting program is available as Ex15_08B. Its outcome demonstrates that even though we have not added any complicated exception-handling code to ComputeValues() , the memory is still freed:

Freeing memory...
No worries: I've caught it!

Tip

Even if your program does not work with exceptions, it remains recommended to always use the RAII idiom to safely manage your resources. Leaks due to exceptions can be harder spot, true, but resource leaks can manifest themselves just as easily within functions with multiple return statements. Without RAII, it is simply too easy to forget to release all resources prior to every return statement, especially, for instance, if someone who did not write the function originally returns to the code later to add an extra return statement.

Standard RAII Classes for Dynamic Memory

In the previous section, we created the DoubleArrayRAII class to help illustrate how the idiom works. Also, it is important that you know how to implement an RAII class yourself. You will almost certainly want to create one several times in your career to manage application-specific resources.

Nevertheless, there are of course Standard Library types that perform the same job as DoubleArrayRAII. In practice, you would therefore never write an RAII class to manage arrays.

A first such type is std::unique_ptr<T[]>. If you include the <memory> header, you can write ComputeValues() as follows:

double* ComputeValues(size_t howMany)
{
  auto values = std::make_unique<double[]>(howMany);  // type unique_ptr<double[]>
  for (size_t i = 0; i < howMany; ++i)
      values[i] = DoComputeValue(i);
  return values.release();
}

In fact, with std::unique_ptr<>, an even better option would be to write it like this:

std::unique_ptr<double[]> ComputeValues(size_t howMany)
{
  auto values = std::make_unique<double[]>(howMany);
  for (size_t i = 0; i < howMany; ++i)
      values[i] = DoComputeValue(i);
  return values;
}

If you return the unique_ptr<> itself from ComputeValues(), you will of course have to slightly adjust the main() function accordingly. If you do, you’ll notice that you no longer need the delete[] statement anymore there. That’s yet another potential source of memory leaks eliminated! Unless you are passing a resource to, for instance, a legacy function, there is rarely any need to release a resource from its RAII object. Just pass along the RAII object itself!

Note

Analogously, returning an object of our DoubleArrayRAII type in ComputeValues() won’t compile because of its deleted copy constructor. But the copy constructor of std::unique_ptr<> is deleted as well, of course—for the same reason. So, clearly that’s not what makes it work for unique_ptr<>. To be able to return a DoubleArrayRAII from a function, you need to enable move semantics for this class. We explain this further in Chapter 17.

Because the resource we are working with here is a dynamic array, you of course know that you could simply use a std::vector<> instead as well:

std::vector<double> ComputeValues(size_t howMany)
{
  std::vector<double> values;
  for (size_t i = 0; i < howMany; ++i)
      values.push_back( DoComputeValue(i) );
  return values;
}

In this case, a vector<> is probably the most appropriate choice. After all, a vector<> is specifically designed to manage and manipulate dynamic arrays. Whichever you prefer, the main message of this section remains as follows:

Tip

All dynamic memory should be managed by an RAII object. The Standard Library offers both smart pointers (such as std::unique_ptr<> and shared_ptr<>) and dynamic containers (such std::vector<>) for this purpose. In the case of smart pointers, make_unique() and make_shared() should always be used instead of new / new[] as well. One important consequence of these guidelines is therefore that the new, new[], delete, and delete[] operators generally have no place anymore in a modern C++ program. Be safe: always, always use an RAII object!

Standard Library Exceptions

Quite a few exception types are defined in the Standard Library. They’re all derived from the std::exception class that is defined in the exception header, and they all reside in the std namespace. For reference, the hierarchy for the standard exception classes is shown in Figure 15-8.
../images/326945_5_En_15_Chapter/326945_5_En_15_Fig8_HTML.gif
Figure 15-8.

Standard exception class types and the headers they are defined in

Many of the standard exception types fall into two groups, with each group identified by a base class that is derived from exception. Both these base classes, logic_error and runtime_error, are defined in the stdexcept header. For the most part, Standard Library functions do not throw logic_error or runtime_exception objects directly, only objects of types derived from these. The exceptions that derive from them are listed in the first two columns of Figure 15-8. The rules for categorizing types in either branch of the exception hierarchy are as follows:
  • The types that have logic_error as a base are exceptions thrown for errors that could (at least in principle) have been detected before the program executed because they are caused by defects in the program logic. Typical situations in which logic_errors are thrown include calling a function with one or more invalid arguments or calling a member function on an object whose state doesn’t meet the requirements (or preconditions) for that particular function. You’ll see concrete examples of both these situations shortly with std::out_of_range and std::bad_optional_access, respectively. You can avoid these types of errors in your program by explicitly checking the validity of the arguments or the object’s state prior to the function call.

  • The other group, derived from runtime_error , is for errors that are generally data dependent and can only be detected at runtime. Exceptions derived from system_error, for instance, generally encapsulate errors originating from calls onto the underlying operating system, such as failing file input or output. File access, as any interaction with hardware, can always fail in ways that cannot a priori be predicted (think disk failures, unplugged cables, network failures, and so on).

There are a lot of exception types in Figure 15-8; and we are not going to grind through where they all originate. Your library documentation will identify when a function throws an exception. We only mention some exception types here that are thrown by the operations that you’re already familiar with:
  • The typeid() operator throws a std::bad_typeid exception if applied on a dereferenced null pointer to a polymorphic type.

  • A std::bad_cast exception is thrown by a dynamic_cast<T&>(expr) operation if expr cannot be cast to T&. An exception can occur only when casting to a reference type T&. When failing to cast to a pointer type T*, dynamic_cast<T*>(expr) simply evaluates to nullptr instead.

  • A std::bad_alloc exception may be thrown by the operators new and new[]. This occurs when memory allocation fails, such as because of a lack of available memory. By extension, any operation that requires dynamic memory allocation may throw this exception as well. Notable examples of such operations include, for instance, copying or adding elements to std::vector<> or std::string objects.

  • Calling the value() member of a std::optional<> object that doesn’t hold a value will throw a bad_optional_access exception. The * and -> operators of optional<> never throw exceptions, though. Using these on an optional<> without a value results in undefined behavior. In practice, this means you will read junk data.

  • The std::out_of_range exception type is used when accessing array-like data structures with an invalid index (usually defined as lying outside the valid range [0, size-1]). Various member functions of std::string throw this exception, as do the at() accessor functions of, for instance, std::vector<> and std::array<>. For the latter, the corresponding overloaded operator—the array access operator[]()—does not perform bounds checking on the index. Passing an invalid index to these operators leads to undefined behavior.

The Exception Class Definitions

You can usefully use a standard exception class as a base class for your own exception class. Since all the standard exception classes have std::exception as a base, it’s a good idea to understand what members this class has because they are inherited by all the other exception classes. The exception class is defined in the exception header like this:

class exception
{
public:
  exception() noexcept;                               // Default constructor
  exception(const exception&) noexcept;               // Copy constructor
  exception& operator=(const exception&) noexcept;    // Assignment operator
  virtual ∼exception();                               // Destructor
  virtual const char* what() const noexcept;          // Return a message string
};

This is the public class interface specification. A particular implementation may have additional non-public members. This is true of the other standard exception classes too. The noexcept that appears in the declaration of the member functions specifies that they do not throw exceptions, as we discussed earlier. The destructor is noexcept by default. Notice that there are no member variables. The null-terminated string returned by what() is defined within the body of the function definition and is implementation dependent. This function is declared as virtual, so it will be virtual in any class derived from exception. If you have a virtual function that can deliver a message that corresponds to each exception type, you can use it to provide a basic, economical way to record any exception that’s thrown.

A catch block with a base class parameter matches any derived class exception type, so you can catch any of the standard exceptions by using a parameter of type exception&. Of course, you can also use a parameter of type logic_error& or runtime_error& to catch any exceptions of types that are derived from these. You could provide the main() function with a try block, plus a catch block for exceptions of type exception:

int main()
{
  try
  {
    // Code for main...
  }
  catch (const std::exception& ex)
  {
    std::cerr << typeid(ex).name() << " caught in main: " << ex.what() << std::endl;
    return 1;  // Return a non-zero value to indicate failure
  }
}

The catch block catches all exceptions that have exception as a base and outputs the exception type and the message returned by the what() function. Thus, this simple mechanism gives you information about any exception that is thrown and not caught anywhere in a program. If your program uses exception classes that are not derived from exception, an additional catch block with ellipses in place of a parameter type catches all other exceptions, but in this case you’ll have no access to the exception object and no information about what it is. Making the body of main() a try block is a handy fallback mechanism, but more local try blocks are still preferred because they provide a direct way to localize the source code that is the origin of an exception when it is thrown.

The logic_error and runtime_error classes each only add two constructors to the members they inherit from exception. Here’s an example:

class logic_error : public exception
{
public:
  explicit logic_error(const string& what_arg);
  explicit logic_error(const char* what_arg);
private:
// ... (implementation specifics)
};

runtime_error is defined similarly, and all the subclasses except for system_error also have constructors that accept a string or a const char* argument. The system_error class adds a member variable of type std::error_code that records an error code, and the constructors provide for specifying the error code. You can consult your library documentation for more details.

Using Standard Exceptions

There is no reason why you shouldn’t make use of the exception classes defined in the Standard Library in your code and a few very good reasons why you should. You can use the standard exception types in two ways: you can either throw exceptions of standard types in your code or use a standard exception class as a base for your own exception types.

Throwing Standard Exceptions Directly

Obviously, if you are going to throw standard exceptions, you should only throw them in circumstances consistent with their purpose. This means you shouldn’t be throwing bad_cast exceptions, for instance, because these have a specific role already. Throwing an object of type std::exception is also less interesting as it is far too generic; it does not provide a constructor you can pass a descriptive string to. The most interesting standard exception classes are those defined in the stdexcept header, derived from either logic_error or runtime_error. To use a familiar example, you might throw a standard out_of_range exception in a Box class constructor when an invalid dimension is supplied as an argument:

Box::Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv}
{
  if (lv <= 0.0 || wv <= 0.0 || hv <= 0.0)
    throw std::out_of_range("Zero or negative Box dimension.");
}

The body of the constructor throws an out_of_range exception if any of the arguments are zero or negative. Of course, the source file would need to include the stdexcept header that defines the out_of_range class. The out_of_range type is a logic_error and is therefore well suited for this particular use. Another candidate here would be the more generic std::invalid_argument exception class. If none of the predefined exception classes suits your need, though, you can derive an exception class yourself.

Deriving Your Own Exception Classes

A major point in favor of deriving your own classes from one of the standard exception classes is that your classes become part of the same family. This makes it possible for you to catch standard exceptions as well as your own exceptions within the same catch blocks. For instance, if your exception class is derived from logic_error, then a catch block with a parameter type of logic_error& catches your exceptions as well as the standard exceptions with that base. A catch block with exception& as its parameter type always catches standard exceptions—as well as yours, as long as your classes have exception as a base.

You could incorporate the Trouble exception class and the classes derived from it into the standard exception family quite simply, by deriving it from the exception class. You just need to modify the class definition as follows:

class Trouble : public std::exception
{
public:
  Trouble(std::string_view errorMessage = "There's a problem");
  virtual const char* what() const noexcept override;
private:
  std::string message;
};

This provides its own implementation of the virtual what() member defined in the base class. Because it is a redefinition of a base class member, we added override to the declaration of what(). Our version of what() displays the message from the class object, as before. We’ve also added noexcept specifications to signal that no exception will be thrown from this member. In fact, we have to because any function override of a noexcept function must be noexcept as well. It is also worth mentioning that noexcept must always be specified after const and before override. The definition for the member function must include the same exception specification that appears for the function in the class definition. The what() function thus becomes the following (virtual or override must not be repeated):

const char* Trouble::what() const noexcept { return message.c_str(); }

Note

We purposely did not add the noexcept specifier to the constructor of Trouble. This constructor, inevitably, has to copy the given error message into the corresponding std::string member variable. This, in turn, inevitably involves the allocation of a character array. And memory allocation can always, at least in principle, go awry and throw a std::bad_alloc exception.

For a more concrete example, let’s return once more to the Box class definition. For the constructor defined in the previous section, it could be useful to derive an exception class from std:: range_error to provide the option of a more specific string to be returned by what() that identifies the problem causing the exception to be thrown. Here’s how you might do that:

#ifndef DIMENSION_ERROR_H
#define DIMENSION_ERROR_H
#include <stdexcept>            // For derived exception classes such as std::out_of_range
#include <string>               // For std::to_string() and the std::string type
class dimension_error : public std::out_of_range
{
private:
  double value;
public:
  explicit dimension_error(double dim)
    : std::out_of_range{"Zero or negative dimension: " + std::to_string(dim)}
    , value{dim} {}
  // Function to obtain the invalid dimension value
  double getValue() const noexcept { return value; }
};
#endif

The constructor provides for a parameter that specifies the dimension value that caused the exception to be thrown. It calls the base class constructor with a new string object that is formed by concatenating a message and dim. The to_string() function is a template function that is defined in the string header; it returns a string representation of its argument, which can be a value of any numeric type. The inherited what() function will return whatever string is passed to the constructor when the dimension_error object is created. This particular exception class also adds a member variable to store the invalid value, as well as a public function to retrieve it, such as to use it in a catch block.

Here’s how this exception class could be used in the Box class definition:

// Box.h
#ifndef BOX_H
#define BOX_H
#include <algorithm>            // For std::min() function template
#include "Dimension_error.h"
class Box
{
private:
  double length {1.0};
  double width {1.0};
  double height {1.0};
public:
  Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv}
  {
    if (lv <= 0.0 || wv <= 0.0 || hv <= 0.0)
      throw dimension_error{ std::min({lv, wv, hv}) };
  }
  double volume() const { return length*width*height; }
};
#endif

The Box constructor throws a dimension_error exception if any of the arguments are zero or negative. The constructor uses the min() template function from the algorithm header to determine the dimension argument that is the minimum of those specified—that will be the worst offender. Note the use of a so-called initializer list to find the minimum of three elements. The resulting expression std::min({lv, wv, hv}) is certainly more elegant than std::min(lv, std::min(wv, hv)), wouldn’t you agree?

The following is an example to demonstrate the dimension_error class in action:

// Ex15_09.cpp
// Using an exception class
#include <iostream>
#include "Box.h"                            // For the Box class
#include "Dimension_error.h"                // For the dimension_error class
int main()
{
  try
  {
    Box box1 {1.0, 2.0, 3.0};
    std::cout << "box1 volume is " << box1.volume() << std::endl;
    Box box2 {1.0, -2.0, 3.0};
    std::cout << "box2 volume is " << box2.volume() << std::endl;
  }
  catch (const std::exception& ex)
  {
    std::cout << "Exception caught in main(): " << ex.what() << std::endl;
  }
}

The output from this example is as follows:

box1 volume is 6
Exception caught in main(): Zero or negative dimension: -2.000000

The body of main() is a try block, and its catch block catches any type of exception that has std::exception as a base. The output shows that the Box class constructor is throwing a dimension_error exception object when a dimension is negative. The output also shows that the what() function that dimension_error inherits from out_of_range is outputting the string formed in the dimension_error constructor call.

Tip

When throwing exceptions, always throw objects, never fundamental types. And the class of these objects should always derive from std::exception, either directly or indirectly. Even if you declare your own application-specific exception hierarchies—which often is a good idea—you should use std::exception or one of its derived classes as the base class. Many popular C++ libraries already follow this same guideline. Using only a single, standardized family of exceptions makes it much easier to catch and handle these exceptions.

Summary

Exceptions are an integral part of programming in C++. Several operators throw exceptions, and you’ve seen that they’re used extensively within the Standard Library to signal errors. Therefore, it’s important that you have a good grasp of how exceptions work, even if you don’t plan to define your own exception classes. The important points that we’ve covered in this chapter are as follows:
  • Exceptions are objects that are used to signal errors in a program.

  • Code that may throw exceptions is usually contained within a try block, which enables an exception to be detected and processed within the program.

  • The code to handle exceptions that may be thrown in a try block is placed in one or more catch blocks that must immediately follow the try block.

  • A try block, along with its catch blocks, can be nested inside another try block.

  • A catch block with a parameter of a base class type can catch an exception of a derived class type.

  • A catch block with the parameter specified as an ellipsis will catch an exception of any type.

  • If an exception isn’t caught by any catch block, then the std::terminate() function is called, which immediately aborts the program execution.

  • Every resource, including dynamically allocated memory, should always be acquired and released by an RAII object. This implies that, as a rule, you should normally no longer use the keywords new and delete in modern C++ code.

  • The Standard Library offers various RAII types you should use consistently; the ones you already know about include std::unique_ptr<>, shared_ptr<>, and vector<>.

  • The noexcept specification for a function indicates that the function does not throw exceptions. If a noexcept function does throw an exception it does not catch, std::terminate() is called.

  • Even if a destructor does not have an explicit noexcept specifier, the compiler will almost always generate one for you. This implies that you must never allow an exception to leave a destructor; otherwise, std::terminate() will be triggered.

  • The Standard Library defines a range of standard exception types in the stdexcept header that are derived from the std::exception class that is defined in the exception header.

Exercises

The following exercises enable you to try what you’ve learned in this chapter. If you get stuck, look back over the chapter for help. If you’re still stuck after that, you can download the solutions from the Apress website ( www.apress.com/source-code/ ), but that really should be a last resort.
  • Exercise 15-1. Derive your own exception class called CurveBall from the std::exception class to represent an arbitrary error, and write a function that throws this exception approximately 25 percent of the time. One way to do this is to generate a random integer between 0 (inclusive) and 100 (exclusive) and, if the number is less than 25, throw the exception. Define a main() function to call this function 1,000 times, while recording the number of times an exception was thrown. At the end, print out the final count. Of course, if all went well, this number should fluctuate somewhere around 250.

  • Exercise 15-2. Define another exception class called TooManyExceptions. Then throw an exception of this type from the catch block for CurveBall exceptions in the previous exercise when the number of exceptions caught exceeds ten. Observe what happens if you neglect to catch the exception.

  • Exercise 15-3. Remember our conundrum with Ex12_11 in Chapter 12? In the Truckload class of that example we were challenged with defining an array subscript operator (operator[]) that returned a Box& reference. The problem was that we had to return a Box& reference even if the index provided to the function was out of bounds. Our ad hoc solution involved “inventing” a special null object, but we already noted that this solution was severely flawed. Now that you know about exceptions, you should be able to finally fix this function once and for all. Choose an appropriate Standard Library exception type and use it to properly reimplement Truckload::operator[]() from Ex12_11. Write a small program to exercise this new behavior of the operator.

  • Exercise 15-4. Create a function readEvenNumber() intended to read an even integer from the std::cin input stream. About 25 percent of the time something really odd happens inside readEvenNumber(), resulting in a CurveBall exception. You can simply reuse code from Exercise 15-1 for this. Normally, however, the function verifies the user input and returns an even number if the user enters one correctly. If the input is not valid, however, the function throws one of the following exceptions:
    • If any value is entered that is not a number, it throws a NotANumber exception.

    • If the user enters a negative number, a NegativeNumber exception is thrown.

    • If an odd number is entered, the function throws an OddNumber exception.

  • You should derive these new exception types from std::domain_error, one of the Standard exception types defined in the <stdexcept> header. Their constructors should compose a string containing at least the incorrectly entered value and then forward that string to the constructor of std::domain_error.

  • Hint: After attempting to read an integer number from std::cin, you can check whether parsing that integer succeeded by using std::cin.fail(). If that member function returns true, the user entered a string that is not a number. Note that once the stream is in such a failure state, you cannot use the stream anymore until you call std::cin.clear(). Also, the non-numeric value the user had entered will still be inside the stream—it is not removed when failing to extract an integer. You could, for instance, extract it using the std::getline() function defined in <string>. Putting this all together, your code might contain something like this:

    if (std::cin.fail())
    {
      std::cin.clear();  // Reset the failure state
      std::string line;  // Read the erroneous input and discard it
      std::getline(std::cin, line);
      ...
  • Once the readEvenNumber() helper is ready, use it to implement askEvenNumber(). This function prints user instructions to std::cout and then calls readEvenNumber() to handle the actual input and input verification. Once a number is read correctly, askEvenNumber() politely thanks the user for entering the number (the message should contain the number itself). For any std::exception that readEvenNumber() throws, askEvenNumber() should at least output e.what() to std::cerr. Any exception that is not a domain_error is to be rethrown, and askEvenNumber() has no idea how to handle these. If the exception is a domain_error, however, you should retry asking for an even number, unless the exception is a NotANumber. If a NotANumber occurs, askEvenNumber() stops asking for numbers and simply returns.

  • Finally, write a main() function that executes askEvenNumber() and catches any CurveBalls that may come out. If it catches one, it should output “…hit it out of the park!” because that’s what you do when life throws you a curveball!

  • Exercise 15-5. The Exer15_05 directory contains a small program that calls upon a C interface to a fictitious database system (the interface is actually a simplified version of the C interface of MySQL). As is common with C interfaces, our database interface returns so-called handles to various resources—resources that need to be freed again explicitly once you’re done using them by calling another interface function. In this case, there are two such resources: the connection to the database and the memory allocated to store the result of a SQL query. Carefully read the interface specification in DB.h to learn how the interface should be used. As this is an exercise, in the program of Exer15_05 these resources may leak under certain conditions. Can you spot any conditions under which the resources are leaked? Since this is an exercise in a chapter on exceptions, these conditions will of course mostly involve exceptions.

  • Hint: To appreciate how subtle error handling can be, ever wondered what std::stoi() does when passed a string that does not contain a number? Check a Standard Library reference—or write a small test program—to find out. Suppose you have customers living at number 10B or 105/5. What will happen in our program? And what if a Russian customer lives at к2, that is, a house whose official address has a street “number” starting with a letter? Similarly, what if for some customers the house number is not filled in? That is, what if the house number stored in the database is an empty string?

  • To fix the resource leaks in our program, you could add explicit resource cleaning statements, add some more try-catch blocks, and so on. For this exercise, however, we’d like you to create and use two small RAII classes instead: one that ensures an active database connection is always disconnected and one that releases the memory allocated for a given query result. Note that if you add cast operators to the RAII classes to implicitly convert to the handle types they encapsulate (and/or to a Boolean), you may not even have to change much of the remainder of the code!

  • Note: With the approach we suggested for this exercise, the main program still uses the C interface, only now it does so while immediately storing all resource handles in RAII objects. This is certainly a viable option, one that we have used in real-life applications. An alternative approach, though, is to use the so-called decorator or wrapper design pattern. You then develop a set of C++ classes that encapsulates the entire database and its query functionalities. Only these decorator classes are then supposed to call upon the C interface directly; the remainder of the program simply employs the members of the C++ decorator classes. The interface of these decorator classes is then designed such that memory leaks are not possible; all resources that the program can ever access shall always be managed by an RAII object. The C resource handles themselves are normally never accessible for the rest of the program. Working out this alternative approach would take us too far from the main topic of this chapter (exceptions), but it is one to keep in mind if ever you have to integrate a C interface within a larger C++ program.