Advanced Methods in Programming Languages (201-2-4411-01)
Notes Week 8 - Michael Elhadad
previous class main page next class

Exceptions in C++

The mechanism of exceptions in C++ uses the following syntactic forms:
try {
 ...body...
} catch ( exceptionTypeSpecification1 exceptionVar1 ) {
 ...handler...
} catch ( exceptionTypeSpecification2 exceptionVar2 ) {
 ...handler...
} ...
  catch ( exceptionTypeSpecificationN exceptionVarN ) {
 ...handler...
} 

To raise an exception:

throw exceptionExpression;

There are 2 special syntactic variants used to propagate an exception and to catch any exception value:

// Catch any exception - irrespective of type
catch (...) {
 handler body;
}

// Only valid from within a handler:
// Rethrows the current exception
throw;

The syntax of the catch clause exception specification is almost the same as the syntax used for declaring function parameters:
catch (E e) ...
catch (E& e) ...
catch (E* e) ...
catch (const E& e) ...
catch (const E *e) ...
What happens actually when throwing exceptions is different from what happens when passing arguments. This is because when an exception is raised, control does not return to the throwing site, and therefore, certain post-operations that can be performed when passing an argument to a function cannot be performed when throwing an exception. For example:
void f() {
  Exception e;
  g(e);
  throw e;
}

void g(Exception& e) {
  // do something with e
}

void h() {
  try {
    f();
  catch (Exception& e) {
    // do something with e
  }
}
When invoking g(e), no copy of e is performed. e is passed by reference. The object e is allocated on the stack inside f, a reference to e is passed to g, which uses it, then when g returns, it returns in the context of f. When f completes, e is de-allocated and the e destructor is invoked.

In contrast, when the exception e is raised, in the dynamic extent of h, the catch clause of h will capture the exception e. A copy of e will still be performed, because when the exception unwinds the stack, the local variable e will be destroyed and de-allocated. Therefore, when the catch clause is reached, the original e object does not exist anymore.

C++ specifies that an object thrown is always copied.

Exception Specifications

In addition to try/catch and throw, the following syntax is used in C++ to declare that a function may only throw a certain list of exceptions:
class C {
  int m(int p) throws ();
  int n(int q) throws (exception, ioexception);
};
If a method is declared with a throw specifier, and the method lets another exception exit from its scope, the C++ runtime engine will translate the un-specified exception to a special exception called "unexpected".

There are some issues which make the use of exception specifiers not convenient (discussed in the references). One form, however, is particularly important, especially for documentation purposes:

class C {
  ~C() throws();  // nothrow warranty
};
The nothrow warranty indicates that the programmer has verified that the function cannot ever throw any exception. It is important to insure that destructors implement the nothrow warranty in general (the motivation is discussed in the references).

Why Use Exceptions

Without exceptions, error conditions must be verified in many locations:
  1. Where the error could occur
  2. In any place that invokes any function in which an error may occur
The effect is that error handling becomes mixed at all levels of the computation, in a style similar to:
const int OK = 1;
const int ERROR1 = -1;
const int ERROR2 = -2;

int f() {
  int g = g();
  if (g != OK) return g;
  int h = h();
  if (h != OK) return h;
  // do something
  ...
  return OK;
}

int g() {
 if (testError) return ERROR1;
 // do something
 ...
 return OK; 
}

int h() {
  if (testError2) return ERROR2;
  // do something
  ...
  return OK;
}
Exception handling allows us to separate the code which deals with error, the one that detects error, and the code that is not concerned with errors in separate blocks. It also eliminates the need for error constants (the int codes) and replaces them with object-oriented constructs (exception classes).

When to Use Exceptions

Exceptions should be used in "exceptional conditions":
  1. They should not be raised for cases that are expected to happen on "legal" data
  2. They should be raised when an operation cannot find any reasonable way to deal with a situation locally.
  3. They should be raised only when the try/catch block is not in the same context as the "throw" location. (Otherwise, instead of a try/catch, use regular if constructs to avoid the exception.)

How to Deal with Exceptions

In C++, one can deal with exceptions in 2 ways:
  1. Using try/catch
  2. Using "resource managers" -- that is, classes whose destructors "clean up" resources.
There are many advantages to using resource managers - and in general, one of the design goals is to limit the usage of try/catch blocks which are difficult to manage.

Exception Safe Code

When writing code in the presence of objects that can throw exceptions, one must take the following decisions:
  1. When invoking code that can potentially throw exceptions, should the caller code deal with the exception or let it propagate upwards to another caller.
  2. If the exception is caught, the caller should "clean up" resources it has allocated and decide whether to restart the invocation (retry), or abort by propagating again the exception (throw) or another exception (exception translation).
When deciding whether it is possible to retry a computation after an exception has been thrown, the caller side must rely on commitments provided by the called objects. Such commitments are classified as "exception safety warranties". There exist several classifications of such warranties. The most frequently used classification is provided in Exception Safety in STLport, by Dave Abrahams:
  1. Basic Guarantee: Even in the presence of exceptions thrown by the callee, the caller does not leak resources. This implies that the caller object will remain in a destructible and usable state even if an exception is thrown.
  2. The Strong Guarantee: If an exception is thrown, the operation has no effects on the object. This corresponds to a "commit or rollback" semantics for the operations on the object.
  3. Nothrow Guarantee: The operation will not emit an exception and any circumstances.

An Exception-Safe Stack Class

To illustrate Exception Safety issues, we will inspect a Stack class and make it exception safe. The Stack class is implemented as a template class because stack operations do not depend on the semantics of the objects stored in the stack. Exception-safety, however, does require exception-safety commitments from the stored type. The job of the stack programmer is to minimize the required commitments on the stored type. The discussion is derived from an article by Cargill (1994) and by Sutter (see references).

The Stack Interface

Consider the following stack interface:
template <class T> class Stack {
public:
  Stack();
  ~Stack();

  Stack(const Stack&);            // Copy constructor
  Stack& operator=(const Stack&); // Assignment operator

  // Accessors
  size_t Size() const;

  // Mutators
  void Push(const T&);
  T Pop(); // If empty throws exception
};

An unsafe Implementation

Consider the following implementation:
template <class T> class Stack {
public:
  Stack();
  ~Stack();

  Stack(const Stack&);            // Copy constructor
  Stack& operator=(const Stack&); // Assignment operator

  // Accessors
  size_t Size() const;

  // Mutators
  void Push(const T&);
  T Pop(); // If empty throws exception

private:
  T* v_;          // Pointer to a memory area big enough for
  size_t vsize_;  // vsize_ T's
  size_t vused_;  // Number of T's actually in use
};

// Default constructor
template<class T>
Stack<T>::Stack() : v_(0), vsize_(10), vused_(0) {
  v_ = new T[vsize_];
}
What happens if the new[] operator throws an exception? The operator new[] works in 2 stages:
  1. It allocates enough memory for vsize_ T objects on the heap.
  2. It then invokes the default constructor of each T object vsize_ times.
An exception can be thrown because:
  1. There is not enough room in the heap. a bad_alloc exception is thrown.
  2. One of the vsize_ constructors throws an exception.
This constructor is exception neutral and exception safe: there is no reason to catch the exception thrown because if it happens we do not leak resources. This is why:
  1. If a bad_alloc exception is thrown by new[], no memory is allocated (this is a guarantee provided by new[] in C++), so there is no reason to clean up. The constructor exits and the Stack object is never constructed.
  2. If one of the T constructors throws an exception, any of the T objects that were properly constructed will be properly destroyed, one by one, and finally operator delete[] will be invoked to release the memory allocated by new[]. Note however, that we impose here the requirement that the T destructor, ~T(), does not throw exceptions, otherwise, the clean up of the constructed T objects might not end up well. We therefore have a requirement on T that ~T() implements the nothrow() guarantee.
  3. If any exception is thrown, we are left in a consistent state: there is no object Stack ever constructed, so we do not need to worry about leaving it in a "safe state" because there is no state.
Note that the following version is also exception safe:
// Default constructor
template<class T>
Stack<T>::Stack() : v_(new T[10]), vsize_(10), vused_(0) {
}
But note that this is only possible because there is only one resource acquisition expression in the initialization list, and there is no dependency among the different initializers.

Destructor

template<class T> Stack<T>::~Stack() throws() {
  delete[] v_; 
}
The statement delete[] cannot throw because we imposed the requirement that ~T() does not throw. In addition, in C++ we know that delete[] does not throw.

Copy

The following "naive" code is not exception safe:
template <class T> Stack<T>::Stack(const Stack<T>& other) {
  v_ = new T[other.vsize_];  // can throw
  vsize_ = other.vsize_;
  vused_ = other.vused_;
  for (int i = 0; i < vused_; ++i) {
      v_[i] = other.v_[i];     // can throw
  }    
}
If the new[] operator throws, nothing bad happens. The exception is propagated as it was in the default constructor case. If, however, during the loop, one of the copy assignment throws, then we have a memory leak: the memory allocated by new[] is not released, and the T's are not destroyed, because the delete[] operator is not applied to v_.

A similar problem arises in the copy assignment. To avoid this problem, we introduce an auxiliary function which is responsible to manage memory allocation:

template <class T> T* NewCopy(const T* src, size_t srcsize, size_t destsize) {
  assert( destsize >= srcsize);
  T* dest = new T[destsize];
  try {
    copy(src, src+srcsize, dest);
  } catch (...) {
    delete[] dest;  // nothrow
    throw;
  }
  return dest;
}
NewCopy can throw, but it does not leak - it manages its resources and propagates the exceptions (note the use of catch(...) and throw;). Using this function we can rewrite the copy constructor in an exception safe manner:
template <class T> Stack<T>::Stack(const Stack<T>& other) :
  v_( NewCopy( other.v_, other.vsize_, other.vsize_ ),
  vsize_( other.vsize_ ),
  vused_( other.vused_ ) {
}
Similarly, the copy assignment is:
template <class T> Stack<T>::operator=(const Stack<T>& other) {
  if (this != &other) {
    T* v_new = NewCopy( other.v_, other.vsize_, other.vsize_ );
    delete[] v_;  // no throw
    v_ = v_new;
    vsize_ = other.vsize_;
    vused_ = other.vused_;
  }
  return *this;  // safe - no copy
}
This illustrates a general principle of exception-safe code: do all the work on the side in temporary variables allocated on the stack, then when all resources are safely acquired, swap with object members and update the object state with only nothrow operations.

Mutation Operations

The push operation must re-allocate enough memory if the stack is full:
template <class T> void Stack<T>::Push(const T& t) {
  if (vused_ == vsize_) {
    T* v_new = NewCopy( v_, vsize_, vsize_*2+1 ); // Can throw
    delete[] v_;  // no throw
    v_ = v_new;   // acquire new resource
    vsize_ = vsize_*2+1;
  }
  v_[vused_] = t; // Can throw
  ++vused_;
}
If NewCopy throws, it takes care of its resources and does not leak, and the stack object is not modified. So this path of execution is strongly safe.

If NewCopy completes, we acquire the new vector using nothrow operations only. When the if block completes, the stack is in a consistent state and no resource is outstanding.

The copy assignment operator used to copy the argument into the stack can throw. In this case, if we required that the T operator= is weakly safe, nothing wrong happens to the stack: vused_ is not updated, the slot vused_ remains in deletable state and no resources are leaked (because T::operator= is weakly safe).

The pop operation is more problematic: The naive implementation is not safe:

template <class T> T Stack<T>::Pop() {
  if (vused_ == 0) {
    throw "Empty stack";
  }
  T t(v_[vused_]); // Can throw
  --vused_;
  return t;        // Can throw
}
The problem is that because the signature of Pop() requires to return a T element by value, the return statement can invoke a copy constructor. In this case, the copy operation can throw. If the return throws, the stack state has already been updated -- and the last element is not accessible anymore. The Pop() operation does not succeed and no try/catch operation can restore the state of the stack in a way that the popped element become accessible again. That is, the way the signature of the Pop() operation is provided, there is no way to make it exception safe.

The solution is to modify the signature and to introduce (as is done in STL) two operations - one for each aspect of the work Pop() was supposed to do:

  1. T& top(); // Return a reference to the top element - throws if empty
  2. void pop(); // Make top element not accessible anymore - no return
Note that it is safe to have top() return a reference to the top element without copying it, because by definition the object remains safely in the stack (this was no true for the previous T Pop() operation which removed the object from the stack and therefore could not return a reference to it). It is now the responsibility of the programmer to ensure that this reference remains valid, and to copy it if needed (which can be done in a safe or protected manner). Note also that a Push() operation can make the top() reference invalid (because it can move the objects in the v_ vector to another location). This must be documented in the Push() and Top() interfaces.

Requirements on Contained Type T

The stack presented here is strongly exception-safe as long as T satisfies the following requirements:
  1. T has a default constructor (can throw).
  2. T has a weakly safe copy constructor.
  3. T has a weakly safe assignment operator.
  4. ~T does not throw.
Note that Stack was made exception-safe while using a single try/catch statement - and even that one could be removed by using an auto_ptr.

We can reduce the requirements on T if we notice that the default constructor and assignment operator are used because we have define v_ as an array of T. If instead we look at T as just a memory area and manage in-place object construction, we can reduce the requirements on T.

Containers which are strongly exception-safe with minimal requirements on the contained type T are provided in some STL implementations. The best such implementation is available in STLPort, an open-source implementation of STL with strong exception guarantees.

General Exception-Safety Rules

3 general exception-safety rules provided by Sutter can be summarized:
  1. Never allow an exception to escape from a destructor, or from an overloaded operator delete() or operator delete[]().
  2. Always use the "resource acquisition is initialization" idiom to isolate resource ownership and management.
  3. In each function, take all the code that might throw an exception and do all the work safely off to the side. Only when you know that the real work has succeeded, should you modify your program state and clean up using nothrow operations only.

References

C++ Exceptions

  1. Exception-Safe Class Design, Part 1: Copy Assignment, H. Sutter, July 1999 (Definition of the three-level exception safety guarantees, how to write an exception-safe copy assignment operator using the P_impl idiom).
  2. Simplify your Exception-Safe Code, Andrei Alexandrescu and Petru Marginean, C/C++ Users Journal, Dec 2000 (Introduces the ScopeGuard template to inject resource clean up code without writing resource manager classes - essential reading, extremely useful in practical programming.)

    Note: The very popular ScopeGuard class has been since integrated into the Loki library.

  3. ACID Programming, H. Sutter, Sept 1999. (Comparison of Exception-safe code with transaction management in databases.)
  4. Pattern Languages for Handling C++ Resources in an Exception-Safe Way, Harald M. Mueller, 2nd Usenix Conference on Object-Oriented Technologies (COOTS), 1996. (Classification of exception-safe techniques in C++).
  5. How a C++ compiler implements exception handling, Vishal Koshhar, Apr 2002 (This gives the "machine level" view of exception handling in a Windows environment - very useful to understand the cost and details of exception handling.)
  6. Exception Safety and Exception Specifications: Are they Worth it?, H. Sutter, June 2001 (Short answer: except as a documentation of the no-throw guarantee, not many reasons to use throw specifications in C++.)
  7. Constructor Failures, H. Sutter, Jan 2000. (What happens when an exception is thrown inside the body of a constructor, and what to do about it.)
  8. Exception-Safe Function Calls, H. Sutter, May 1999. (Make sure the order of execution of sub-expressions is understood otherwise resource release is impossible.)
  9. Uncaught Exceptions, H. Sutter, Nov 1998. (When exceptions are not caught, what can be done in last resort).
  10. Code Complexity - Part II, H. Sutter, Sep 1997.

Java Exceptions

  1. Java Exceptions Tutorial (Sun)
  2. Understanding Java Exceptions, Chuck Allison, C/C++ Users Journal, April 2001.

Last modified Apr 27, 2003 / 02 Apr 2019 Michael Elhadad