[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