Advanced C++

Exceptions

Máté Cserép

October 2015, Budapest

Overview

  • When and why to use exceptions?
  • Exceptions overview, history, setjmp/longjmp
  • Standard library exceptions
  • Exceptions in constructors and destructors
  • Function try blocks
  • Polymorphic exceptions, catch by value vs. reference
  • There is finally in Java / C# -what to do in C++?
  • Resource management and exceptions, RAII
  • Exception-safe programming
  • Exception specification in function prototype
  • Noexcept operator and specifier

What is exceptional?

There are situations in runtime, where program runs in exceptional way. This could be handle in many different ways: using boolean return values, assertions or exceptions. The old C way:

struct record { /* ... */ };

record r;
extern int errno;
FILE *fp;

if ((fp = fopen("fname", "r")) != NULL) {
  fprintf(stderr, "can't open file %s\n", "fname");
  errno = 1;
}
else if (!fseek(fp, 0L, n*sizeof(r))) {
  fprintf(stderr, "can't find record %d\n", n);
  errno = 2;
}
else if (1 != fread(&r, sizeof(r), 1, fp)) {
  fprintf(stderr, "can't read record\n");
  errno = 3;
}
else {
  /* ... */
}
#include <cassert>

struct record { /* ... */ };

int main()
{
  /* ... */
  record *ptr = /* ... */
  assert(ptr);
  /* ... */
}

Goals of exception handling

  • type-safe transmission of arbitrary data from throw point to handler point

  • no extra code/space/time penalty if not used

  • every exception is caught by the appropriate handler

  • groupping of exceptions

  • multithread environment support

  • cooperation with other languages (like C)

But the extra cost in code and runtime most cases unavoidable. Scott Meyers states that declaring exceptions, even if it never used costs extra 10% both in extra code and extra runtime.

setjump / longjump

jmp_buf x;

void f()
{
  longjmp(x,5);
}

Since PL/I programming languages have techniques for handling exceptional situations. Between the archaic ones the most famous is the setjmp/longjmp in the C language.

int i = 0;
if ((i = setjmp(x)) == 0)
{
  f();
}
else
{
  switch(i)
  {
    case  1: /* ... */ break;
    case  2: /* ... */ break;
    default: fprintf(stdout, "error code = %d\n", i); break;
  }
}

Components of exception handling

  • try block
  • catch handlers
  • throw expression
void f()
{
  /* ... */
  T1 e;
  throw e;    /* throws exception of type T */

  // alternatively:
  throw T1();  /* throws default value of T */
}

A handler H triggers on exception E if:

  1. H has same type as E
  2. H is unambiguous base type of E
  3. H and E pointers or references and 1 or 2 holds
try
{
  f();
  /* ... */
}
catch (T1 e1) { /* handler for T1 */ }
catch (T2 e2) { /* handler for T2 */ }
catch (T3 e3) { /* handler for T3 */ }

Throw:

Catch:

Exception hierarchies

In object-oriented programming languages the main technique for expressing specialization and generalization is the inheritance hierarchy. Inheritance in exceptions are particularly important for grouping exceptions in catch handlers.

try {
  /* ... */
}
catch(Derived1 d1) { /* ... */ }
catch(Derived2 d2) { /* ... */ }
catch(Derived3 d3) { /* ... */ }
catch(Base     b)  { /* ... */ }
class net_error  { /* ... */ };
class file_error { /* ... */ };
class nfs_error
  : public net_error, public file_error
{ /* ... */ };

void f()
{
  try
  {
    /* ... */
  }
  catch(nfs_error  e) { /* ... */ }
  catch(file_error e) { /* ... */ }
  catch(net_error  e) { /* ... */ }
}

Example:

Usage:

Exceptions in the standard library

class exception                               {};  // <exception>
class bad_cast             : public exception {};  // dynamic_cast
class bad_typeid           : public exception {};  // e.g. typeid(0)
class bad_exception        : public exception {};  // unexpected()
class bad_weak_ptr         : public exception {};  // weak_ptr -> shared_ptr (C+11)
class bad_function_call    : public exception {};  // std::function::operator() (C+11)
class bad_alloc            : public exception {};  // new
class bad_array_new_length : public bad_alloc {}   // new T[-1] (C+11)

class runtime_error     : public exception {};     // errors that can only be detected during runtime
class range_error       : public runtime_error {}; // e.g. floating point ovf or unf
class overflow_error    : public runtime_error {}; // e.g. integer overflow INT_MAX+1
class underflow_error   : public runtime_error {}; // e.g. integer underflow INT_MIN-1
class system_error      : public runtime_error {}; // e.g. std::thread constr.

class logic_error       : public exception {};     // errors that could be prevented before the execution
class domain_error      : public logic_error {};   // e.g. std::sqrt(-1)
class invalid_argument  : public logic_error {};   // e.g. bitset char != 0 or 1
class length_error      : public logic_error {};   // e.g. str.resize(-1)
class out_of_range      : public logic_error {};   // e.g. bad index in cont. or string
class future_error      : public logic_error {};   // e.g. duplicate get/set (C++11)

class ios_base::failure : public exception {};     // base class for I/O exceptions (before C+11)
class ios_base::failure : public system_error {};  // base class for I/O exceptions (C+11)

Comparing to e.g. the Java language C++ has only a few standard exception class.

Exceptions in the standard library

namespace std
{
  class exception
  {
  public:
    exception();
    exception(const exception& other);
    virtual ~exception();
    exception& operator=(const exception& other);
    virtual const char *what() const;
  private:
    /* ... */
  };
}

Definition of std::exception

Exceptions in constructors

class X;

int main()
{
  try
  {
    X *xp = new X();
  }
  catch(...) { /* space allocated, but xp was not set */ }
               // still no problem: system deallocates space
}

Constructors have no return value. Still, a constructor sometimes must propagate failure. There are several method for this: error flags, globals, and most importantly exceptions. However exceptions in constructors have special behaviour.

class X
{
public:
  X(int i) { p = new char[i]; init(); }
  ~X()     { delete[] p; }             // must not throw exception
private:
  void init() { /*... throw ... */ }   // BAD: destructor won't run!
  char *p;                             // constructor was not completed
};

However:

Member initializaion

class X
{
public:
  X(int i, int j) : y(i), z(j) { }  // y(i) or z(j) throws exception
                                    // but X must not emit any
private:
  Y y;
  Z z;
};

class X
{
public:
  X(int i, int j) try : y(i), z(j)  // y(i) or z(j) throws exception
  { }
  catch(...)
  {
    // we get here if either y(i) or z(j) throws exception
    // if y(i) throws, then z uninitialized
    // if z(j) throws, then ~Y::Y() has already executed
    // nothing to do here ...
  }
private:
  Y y;
  Z z;
};

If a sub-object initializer throws exception then sub-object has not been created: no object without sub-object, so constructor will throw exception!

Member initialization

#include <iostream>

class X
{
public:
  X() { throw 1; }
};
class Y
{
public:
  Y() try: x() { }
  catch(...) { /* throw; */ }
private:
    X x;
};

int main() try
{
  Y y;
  return 0;
}
catch (int i)
{
  std::cerr << "exception: " << i << std::endl;
}

Exception in destructors

Destructor can be called in one of two ways:

X::~X() throw()
try
{
  /* ... */
}
catch(...) { /* ... */ }

The rule of thumb for exceptions in destructor:

They must never throw

  • normal mode
  • call during exception handling

in the later emitting an exception cause undefined behaviour,
most likely terminate()

X::~X()
{
  if (!uncaught_exception())  {
    // code that could throw...
  }
  else {
    // code must not throw
  }
}

Polymorphic exceptions

struct BaseException
{
  virtual void what() { std::cout << "BaseException" << std::endl; }
};

struct DerivedException : BaseException
{
  void what() { std::cout << "DerivedException" << std::endl; }
};

void test()
{
  throw DerivedException();
}

int main()
{
  try
  {
    test();
  }
  catch(BaseException ex)
  {
    ex.what();
  }
  return 0;
}

Catching exceptions by value is a bad idea, object slicing would occur.

Throw by value, catch by reference

struct BaseException
{
  virtual void what() { std::cout << "BaseException" << std::endl; }
};

struct DerivedException : BaseException
{
  void what() { std::cout << "DerivedException" << std::endl; }
};

void test()
{
  throw DerivedException();
}

int main()
{
  try
  {
    test();
  }
  catch(BaseException &ex)
  {
    ex.what();
  }
  return 0;
}

Throwing pointers is also bad in most cases, because of memory management.

Resource problems

There are several resource allocation problem connecting to exception handling. These are mostly the result of the procedural usage of resource allocation and free. 

void f()
{
  char *ptr = new char[1024];

  g(ptr);     // can throw exception
  h(ptr);

  delete [] ptr;
}
void f()
{
  try
  {
    char *ptr = new char[1024];

    g(ptr);
    h(ptr);

    delete [] ptr;
  }
  catch(...)
  {
    delete [] ptr;
    throw;  // rethrow original exception
  }
}

 One technique to solve the problem is what Stroustrup called: Resource Allocation Is Initialization

Resource Allocation Is Initialization

template <typename T>
class Res
{
public:
  Res(int i) : p( new T[i] ) {}
  ~Res()   { delete [] p; }
  T* ptr() { return p; }
private:
  T* p;
};

void f()
{
  Res r(1024);

  g(r.ptr);   // can throw exception
  h(r.ptr);
}

RAII, also called Resource Acquisition Is Initialization

C++ does not support finally blocks. C++ instead supports RAII.

Exceptions and the evaluation order

void f( int expr1, int expr2);
int  g( int expr);
int  h( int expr);

f( g( expr1), h(expr2) );

What will be the evaluation order of the last expression?

Evaluation order of a constructor:

  1. It allocates memory
  2. Constructs the object at that memory
  3. If construction fails, memory will be freed
  4. Otherwise returns with pointer to the memory
template <class T>
void f( T1*, T2* );

f( new T1, new T2);

What will be the evaluation order of the constructors?

Exceptions and the evaluation order

template <class T1, class T2>
void f( T1*, T2* );

f( new T1, new T2);

What will be the evaluation order constructors?

A possible scenario:

  1. Allocates memory for T1
  2. Constructs the object T1
  3. Allocates memory for T2
  4. Constructs the object T2
  5. Call f()

Another possible scenario:

  1. Allocates memory for T1
  2. Allocates memory for T2
  3. Constructs the object T1
  4. Constructs the object T2
  5. Call f()

If 3. or 4. fails destructor of T1 won't be executed, which is a memory leak

If 3. fails then T1 is deallocated, but T2 is not, which is a memory leak
If 4. fails then T2 is deallocated, but T1 is not, which is a memory leak

Exceptions and the evaluation order

template <class T1, class T2>
f( auto_ptr<T1>, auto_ptr<T2>);

f( auto_ptr<T1>(new T1), auto_ptr<T2>(new T2));

What will be the evaluation order constructors?

A possible scenario:

  1. Allocates memory for T1
  2. Constructs the object T1
  3. Allocates memory for T2
  4. Constructs the object T2
  5. Constructs auto_ptr<T1>
  6. Constructs auto_ptr<T2>
  7. Call f()

Same problems.

template <class T1, class T2>
f( auto_ptr<T1>, auto_ptr<T2>);

auto_ptr<T1> t1(new T1);
auto_ptr<T2> t2(new T2);
f(t1, t2);

Solution:

Strong exception safety

template <class T>
matrix<T> matrix<T>::operator=( const matrix &other)
{
  if ( this != &other )
  {
    delete [] v;
    copy( other);
  }
  return *this;
}

template <class T>
void matrix<T>::copy( const matrix &other)
{
  x = other.x;
  y = other.y;
  v = new T[x*y];
  for ( int i = 0; i < x*y; ++i )
    v[i] = other.v[i];
}
template <class T>
matrix<T> matrix<T>::operator=( const matrix &other)
{
  if ( this != &other )
  {
    matrix temp(other);
    T *oldv = v;

    // swap() in STL
    x = temp.x;
    y = temp.y;
    v = temp.v;
    temp.v = oldv;
  }
  return *this;
}

Dangerous version

Exception safe version

Exception safety

  1. Constructor function try block good only for translating exception
  2. Destructor function try block is good for almost nothing
  3. Other functions try block are equivalent with try block inside the function body
  4. Do unmanaged resource allocation in constructor body, never in the initializer list (or more better: use resource objects)
  5. Clean up unmanaged resources in local try blocks
  6. Exception specification of a constructor must be the union of all possible exceptions that could be thrown by base and member sub-objects
  7. Use Pimpl idiom if a base object can throw but you want survive
  8. Prefer "Resource Allocation Is Initialization"
  9. Perform every explicite resource allocation in its own statement

Exception specification

In Java exception specification is a mandatory part of method declaration. In C++ exception specification is optional - and deprecated since C++11. When used, the behaviour is different.

class E1;
class E2;

void f() throw(E1)  // throws only E1 or subclasses
{
  /* ... */
  throw E1();     // throws exception of type E1
  /* ... */
  throw E2();     // calls unexpected() which most likely calls terminate()
}
void f() try
{
  /* ... */
}
catch(E1)  { throw; }
catch(...) { std::unexpected(); }

Alternatively:

Usage of bad_exception

In Java exception specification is a mandatory part of method declaration. In C++ exception specification is optional - and deprecated since C++11. When used, the behaviour is different.

class E1;
class E2;

void f() throw(E1, std::bad_exception)  // throws only E1 or subclasses
{                                       // or bad_exception 
  /* ... */
  throw E1();     // throws exception of type E1
  /* ... */
  throw E2();     // calls unexpected() which throws bad_exception
}
typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler);

typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler);

Unexpected and terminate handlers:

Note: terminate() by default calls abort()

noexcept operator and specifier

bool noexept(expr);

void f() noexcept(expr) { }
void f() noexcept(true) { }
void f() noexcept       { }

noexcept was added rather than just removing all throw specifiers other than throw() because noexcept is more powerful. noexcept can have a parameter which resolves into a boolean. If true, then the noexcept sticks. If the boolean is false, then the noexcept doesn't stick and the function may throw.

Can improve efficiency:

void f() throw() { }

noexcept examples

void fun1() noexcept
{
  int i = 0;    // noexcept
  int * p = &i; // noexcept
  p = nullptr;  // noexcept
}
 
void fun2() noexcept
{
  fun1();
  fun1();
}

template <typename T>
void f() noexcept(noexcept(T::g()))
{
  g();
}

void pair::swap(pair& p)
  noexcept(noexcept(swap(first, p.first)) &&
           noexcept(swap(second, p.second)));

Exception safety in the Standard Library

void f(vector<X>& v, cosnt X& g)
{
    v[2] = g;                   // X::operator= might throw exception
    v.push_back(g);             // vector<X> 's allocator might throw exception
    sort(v.begin(), v.end());   // X's less than might throw exception
    vector<X> u = v;            // X's copy constructor might throw exception
}

Exceptions may occur in several cases with the STL:

Theoretically there are two possibilities:

  1. No guarantees: the container possibly corrupted.
  2. Strong guarantees: all object will keep the invariant.

Exception safety in the Standard Library

In practice in the standard library all the operations are categorized by the following:

  • Basic guarantee for all operations: basic invariants are kept, no memory leak or other resource problem
     
  • Strong guarantee for key operations: the operation succeeds or has no effect. This guarantee is provided for some key operations, like push_back(), insert() on a list.
     
  • Nothrow guarantees for some operation: some operations are guaranteed not to throw exception, like pop_back() or swap().

Note: basic and strong guarantee suppose, that the user operations do not leave the container operations in invalid state.

Exception safety in the Standard Library

Guarantees for vector and deque:

  • push_back(), push_front(): strong guarantee
  • insert(): strong, unless copy constructor or assignment of user class throws
  • erase(): not throws, unless copy constr. or assignment of user class does it
  • pop_back(), pop_front(): nothrows
     

Guarantees for list:

  • insert(), push_back(), push_front(): strong guarantee
  • erase(), pop_back(), pop_front(), splice(), reverse(): nothrows
  • remove(), remove_if(), unique(), sort(), merge(): not throws, unless the user class predicate or comparison function does it

Exception safety in the Standard Library

Guarantees for associative containers:

  • insert(): strong guarantee
  • erase(): nothrows

Guarantees for string:

  • erase(), insert(), push_back() and swap(): strong guarantee

Advanced C++: Exceptions

By Cserép Máté

Advanced C++: Exceptions

  • 168