Doc. no.   WG21/N2061=06-0131
Date:        2006-09-08
Project:     Programming Language C++
Reply to:   Beman Dawes <bdawes@acm.org>

Library Exception Propagation Support

This paper is in two parts:

The proposals in this paper are preliminary, and intended to put the issues on the table for further discussion.

Part One: Exception propagation from threads

Motivation

In a thread of execution other than main(), what is useful yet safe behavior if no matching exception handler is found after an exception is thrown?

Users of the Boost threading library, which does not currently do anything special with exceptions, have asked time-and-time again that the exception to be caught by the thread library mechanism and saved, and then re-thrown by the thread object's join() function or equivalent.

Discussion

Within a thread of execution, an exception will presumably cause a search for an exception handler as in any C++ program. Absent any thread library support for exception propagation, if no matching exception handler is found within the thread of execution, then std::terminate() will be called (15.3/9).

But just as in single-thread programs, it is highly desirable that exceptions propagate from a function to its caller. Because a thread's processing function is in a different thread of execution from its joining function doesn't change that. If exceptions do not propagate out of a thread of execution to its joining function, programmers are forced to construct ad hoc error recovery techniques of the worst kind, such as catching exceptions themselves and converting them to error codes (which are too often ignored by the joiner, with disastrous effects).

Requirements

  1. An exception is never ignored; if no handler is found, if the thread of execution is never joined, or if the thread of execution is detached, std::terminate() is called.

Desirable, but not absolute requirements

  1. The exact exception type is propagated and the exact type can be caught without slicing. See example program, and note that catch #1 is desired to work.  The proposed library solution does achieve this.
  2. Works for both Standard Library and user-declared types. The proposed library solution does achieve this.
  3. Works for all types, with no need for a common base class or special exception launcher. Only a core language solution can achieve this.

Interaction with other proposals

Proposals dealing with a C++ memory model that supports multi-threading will presumable change 15.3, Handling an exception, paragraph 9 as indicated:

If no matching handler is found in a program the current thread of execution, the function std::terminate() is called; whether or not the stack is unwound before this call to std::terminate() is implementation-defined (15.5.1).

Part One: Proposed changes

Add  a class thread_exception_error derived from std::_runtime_error, details to be supplied.

To the launcher function, add:

Effects: If an exception is thrown by the thread function, and the thread has not been detached, the exception is caught and saved (see join and destructor). Otherwise, std::terminate() is called.

To the join function, add:

Throws: If an exception from the thread function has occurred:

To the thread destructor, add:

Effects: If the thread object's thread-of-execution ended with an uncaught exception and the join function has not been called, call std::terminate().

Open Issues


Part Two: Exception propagation mixin

Implementation of exception propagation requires two actions not directly supported by the C++03 exception handling mechanism:

There are three known approaches:

  1. Language changes to allow cloning and throwing objects knowing only their base class.
     
  2. A library based approach, requiring exceptions that may be propagated according to their dynamic type be cloned and thrown by a special library-supplied mechanism.
     
  3. A library based approach, requiring exceptions that may be propagated according to their dynamic type be derived from a special library-supplied mixin base class.

The question of a core language approach versus a library approach was considered at the Redmond ad hoc threads meeting in August, 2006. There was consensus for going forward with both core language support and library support. If core language support is accepted, the library approach may still be of some value for C++03 compilers, and any cases not covered by core language support.

This proposal relies on a library solution to achieve the desired "join re-throws" behavior.

Part Two: Proposed changes

To a header to be decided, add:

namespace std
{
  namespace tr2
  {
    class cloneable
    {
    public:
      virtual cloneable *  clone_self() const;
      virtual void         throw_self() const;
      virtual ~cloneable();
    };
  }
}

Class cloneable

Class cloneable provides a mixin base class for objects that need to clone themselves or throw themselves.

virtual cloneable * clone_self() const;

Returns: new cloneable(*this)

virtual void throw_self() const;

Throws: *this

Rationale for mixin approach

Users will need to write catch clauses for specific exception classes, both Standard Library and user-defined. To implement this behavior, it must be possible save and re-throw exceptions without knowing anything more than their base class. The clone_self and throw_self functions make this possible. Using a mixin base class allows degraded functionality even when the version of the Standard Library used does not itself use the mixin. It also works for user-defined exception types no derived from std::exception.

Pete Becker pointed out that the C++ runtime support knows the types, so it would be possible to add a clone operator to the language, and change the throw operation rules so the correct type is thrown on the rethrow. This current proposal is intended to work within the current language.

Impact on the Standard

This is a pure library proposal, suitable for TR2. It can be portably implemented with any C++03 compiler.

Implementation experience with both parts 1 and 2

Boost threads was hacked to provide the proposed functionality. The following program illustrates the results:

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/cloneable.hpp>
#include <stdexcept>
#include <iostream>
boost::mutex cout_mutex; // cout race prevention;

class my_exception
  : public std::runtime_error, public virtual boost::cloneable
{
public:
  my_exception( const std::string & s ) : runtime_error( s ) {}
  ~my_exception()                    {}
  my_exception *  clone_self() const { return new my_exception(*this); }
  void            throw_self() const { throw *this; }
};

class foo
{
  int m_argc;
public:
  foo( int argc ) : m_argc( argc ) {}
  void operator()()
  {
    boost::mutex::scoped_lock lock(cout_mutex);
    std::cout << "starting thread function" << std::endl;
    if ( m_argc > 1 ) throw my_exception("\"what-string\"");
    std::cout << "normal return from thread function" << std::endl;
  }
};

int main( int argc, char * argv[] )
{
  foo child(argc);
  std::cout << "starting thread" << std::endl;
  boost::thread t(child);

  {
    boost::mutex::scoped_lock lock(cout_mutex);
    std::cout << "calling join" << std::endl;
  }
  try { t.join(); }

  catch ( const my_exception & ex )  // catch #1
    { std::cout << "caught my_exception: " << ex.what() << std::endl; }
  catch ( const std::exception & ex )  // catch #2
    { std::cout << "caught std::exception: " << ex.what() << std::endl; }

  std::cout << "exiting program" << std::endl;
  return 0;
}

When invoked with no arguments, the output is:

starting thread
starting thread function
normal return from thread function
calling join
exiting program

Invoked with any argument, the output is:

starting thread
starting thread function
calling join
caught my_exception: "what-string"
exiting program

Acknowledgements

An early version of this paper was discussed at the Redmond ad hoc threads meeting in August, 2006. Many participants contributed suggestions, and there was a strong consensus to go forward with a library based approach to the threading issues, and at the same time pursue both library and core language approaches to exception cloning and rethrowing. Alisdair Meredith suggested the mixin approach rather than changing std::exception, and provided an alternative approach that did not require changes to exception classes (but did require changes to code throwing exceptions.)


Copyright Beman Dawes 2006