[cxx-abi-dev] Details missing for EH 2.4.1 Overview of Throw Processing

John McCall rjmccall at apple.com
Wed Dec 21 00:36:15 UTC 2011


On Dec 20, 2011, at 2:30 PM, Dennis Handly wrote:

>> From: John McCall <rjmccall at apple.com>
>>>> We only terminate if an exception is thrown after the initialization is
>>>> complete, e.g.  by a destructor of that full-expression,
>>> 
>>> So does this need to be mentioned for __cxa_end_catch or is it the generated
>>> cleanup code for the throw?
> 
>> I don't know what you mean.  __cxa_end_catch is not required
>> as part of the generated code for a throw expression.  I don't think it
>> ever was.
> 
> I'm saying that __cxa_end_catch is the code that destructs the
> full-expression, if you elide the copy construction.

This conversation would be substantially easier if you looked
up terms like "full-expression" that you don't understand.

I'll break this down.  Suppose we have code like this:
  extern std::string cause;
  extern std::exception make_exception(const std::string &);
  cause = std::string("failure"), throw make_exception("didn't work");

The entire last line is a full-expression:  it's an expression that's not
part of another, larger expression.  This is the granularity at which
temporaries are destroyed.
The part starting at 'throw' is the throw-expression.
The part starting at 'make_exception' is the exception operand.

Here we formally have three temporaries, created in the following order:
  - the std::string created in the LHS of the comma,
  - the std::string created for the argument to make_exception, and
  - the result of make_exception.
Note that the actual exception object is not formally a temporary.  It is
never destructed along any path as part of evaluating this expression.
As soon as it completes construction — and specifically, before any
of the temporaries are destroyed — it is thrown.

Copy elision doesn't really have a significant impact.  Without it, the
generated code for this example looks basically like this, with some
flexibility about in exactly what order the exception is allocated and
freed:

  std::string::string(&temp0, "failure");

  // On the unwind path out of this, call:
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  std::string::operator=(&cause, &temp0);

  exn = __cxa_allocate_exception(sizeof(std::exception));

  // On the unwind path out of this, call:
  //   __cxa_free_exception(exn)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  std::string::string(&temp1, "didn't work");

  // On the unwind path out of this, call:
  //   std::string::~string(&temp1)
  //   __cxa_free_exception(exn)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  make_exception(&temp2, &temp1);

  // On the unwind path out of this, call:
  //   std::exception::~exception(&temp2)
  //   std::string::~string(&temp1)
  //   __cxa_free_exception(exn)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  std::exception::exception(exn, &temp2)

  // On the unwind path out of this, call:
  //   std::exception::~exception(&temp2)
  //   std::string::~string(&temp1)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  __cxa_throw(exn, &typeid(std::exception), &std::exception::~exception);

With copy elision, the code looks like this:

  std::string::string(&temp0, "failure");

  // On the unwind path out of this, call:
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  std::string::operator=(&cause, &temp0);

  exn = __cxa_allocate_exception(sizeof(std::exception));

  // On the unwind path out of this, call:
  //   __cxa_free_exception(exn)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  std::string::string(&temp1, "didn't work");

  // On the unwind path out of this, call:
  //   std::string::~string(&temp1)
  //   __cxa_free_exception(exn)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  make_exception(exn, &temp1);

  // On the unwind path out of this, call:
  //   std::string::~string(&temp1)
  //   std::string::~string(&temp0)
  // and then unwind the enclosing scopes.
  __cxa_throw(exn, &typeid(std::exception), &std::exception::~exception);

Regardless of whether copy elision occurs, __cxa_end_catch has
the potential to throw, because it has the responsibility to destroy the
actual exception object ('exn').

By the time that the unwinder enters a landing pad for a catch clause,
all of these temporaries have already been destructed.  The only object
that survives is the exception object, which, as I mentioned, is not a
temporary.

>>> g++ seems to not disallow that throw in __cxa_end_catch.
>>> aC++ does too but does get lost if a catch is present.
> 
>> In general, the generated call to __cxa_end_catch at the end of a
>> catch clause can throw.  You can prove that it can't in some cases,
>> based on the caught exception object type or the CFG of the catch clause.
> 
> I thought you said that the destructor can't throw and if it does, it calls
> terminate?

If a destructor throws during unwinding, we must call terminate.  But
only some calls to __cxa_end_catch occur during unwinding:  only if
you throw out of a catch clause.  If control falls out of a catch clause,
and the destructor for the caught exception object throws an
exception, that new exception just starts propagating.

In the paragraph you quoted here, I was discussing a very minor
optimization where you deduce that an exception object cannot
have a destructor based on the type that was caught.  For example,
in the catch for a 'void *', you know that __cxa_end_catch
will not throw, because that catch type cannot catch any objects
with destructors.

John.


More information about the cxx-abi-dev mailing list