Handling Exceptions

An exception interrupts the normal flow of control in a program. An exception throws an object from the point where the exception is raised to the point where the exception is handled. In between those points, function calls are aborted, and local scopes are abruptly exited. Local and temporary objects in those scopes are destroyed.

This section discusses exception handling in general and the try statement in particular. For information about other aspects of exception handling, see Chapter 3, which describes the throw expression, and Chapter 5, which describes function try blocks and throw specifications. See also <exception> and <stdexcept> in Chapter 13 for information about the standard exception classes and related functions.

Tip

As the name implies, an exception is used for exceptional circumstances, such as indexing a vector with an index that is out of bounds. The intention in C++ is that try statements should have near zero cost, but throwing and handling an exception can be expensive in terms of performance.

A try statement establishes a local scope plus exception handlers. A try statement begins with the try keyword followed by a compound statement and one or more catch handlers. Each catch handler has an exception type in parentheses followed by a compound statement. The exception type can be a sequence of type specifiers followed by an optional declarator, or it can be an ellipsis:

trycompound-statement
catch(type 
            declarator) compound-statement
catch(type) compound-statement
catch(...) compound-statement

Each compound statement (after try and for each exception handler) forms a separate local scope, following the rules for any other compound statement.

Every time a local scope is entered—for example, by calling a function or by entering a compound statement or other statement that establishes a scope—you can think of the scope as pushing an entry on an execution stack. When execution leaves the scope, the scope is popped from the stack. Each try statement is also pushed on the stack.

When an exception is thrown, the execution stack is unwound. Local scopes are popped from the stack (destroying any automatic objects that were declared in each scope) until a try statement is found. The try statement is popped from the exception stack. Then the type of the exception object is compared with each exception handler. If a match is found, the exception object is copied to the locally-declared exception object, and execution transfers to the compound statement for that handler. After the handler finishes, the exception object is destroyed, and execution continues with the statement that follows the try statement (and its handlers).

If no matching handler is found, the stack continues to be popped until the next try statement is reached. Its handlers are compared with the exception type, and the process repeats until a matching handler is found. If no try statement has a matching handler, and the entire execution stack is popped, the terminate function is called. (See <exception> in Chapter 13.)

An exception handler can throw an exception, in which case, the usual rules apply for unwinding the stack. Note that the try statement has already been popped from the stack, so the same try statement cannot catch the exception. The handler can throw a new exception or rethrow the existing exception (in which case, the exception object is not freed yet).

A catch handler can use an ellipsis to mean it catches any type of exception. When an ellipsis is used, the handler must be last in the list of exception handlers.

Handlers are checked in order. The type in each catch clause is compared with the type T of the exception object according to the following rules:

  • If type is the same as T or T & (not considering cv-qualifiers), the handler matches.

  • If type is a base class of T, the handler matches.

  • If type is a pointer that can be converted to T using a standard conversion (Chapter 3)—e.g., type is void*—the handler matches.

  • An ellipsis (...) always matches.

Thus, it is important to put the most specific exceptions first, and put base classes later. If you use an ellipsis, it must be last.

A common idiom is to throw an exception object and catch a reference. This avoids unnecessary copies of the exception object.

Example 4-10 shows a typical use of a try statement. It also shows a try function block, which is covered in Chapter 5.

Example 4-10. Throwing and catching exceptions
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <numeric>
#include <ostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

class bad_data : public std::out_of_range {
public:
  bad_data(int value, int min, int max)
  : std::out_of_range(make_what(value, min, max))
  {}
private:
  std::string make_what(int value, int min, int max);
};

std::string bad_data::make_what(int value, int min, int max)
{
  std::ostringstream out;
  out << "Invalid datum, " << value << ", must be in [" <<
          min << ", " << max << "]";
  return out.str(  );
}

// Read a set of numbers from an input stream. Verify that all data is within the
// defined boundaries. Throw bad_data if any data is invalid. If an exception is 
thrown, tmp's destructor is automatically called (if it has a destructor).
template<typename T, typename charT, typename traits>
void getdata(std::basic_istream<charT,traits>& in,
             std::vector<T>& data, T min, T max)
{
  T tmp;
  while (in >> tmp)
  {
    if (tmp < min)       throw bad_data(tmp, min, max); 
    else if (tmp > max)
       throw bad_data(tmp, min, max); 
    else
      data.push_back(tmp);
  }
}

// Arbitrary precision integer
class bigint {
public:
  bigint(  );
  ~bigint(  );
  ...
};

int main(int argc, char** argv)
{
  using namespace std;
  if (argc < 2) {
    cerr << "usage: " << argv[0] << " FILE\n";
    return EXIT_FAILURE;
  }

  vector<bigint> data;
  ifstream in(argv[1]);
  if (! in) {
    perror(argv[1]);
    return EXIT_FAILURE;
  }
   try { 
    getdata(in, data, bigint(0), bigint(10));
   } catch (const bad_data& ex) { 
    cerr << argv[1] << ": " << ex.what(  ) << '\n';
    return EXIT_FAILURE;
   } catch(...) { 
    std::cerr << "fatal error: unknown exception\n";
    return EXIT_FAILURE;
  }

  if (data.size(  ) == 0)
    cout << "no data\n";
  else {
    bigint sum(accumulate(data.begin(),data.end(  ),bigint(  )));
    std::cout << "avg=" << sum / data.size(  ) << '\n';
  }
}

Get C++ In a Nutshell now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.