Copying Exceptions in C++0x
Tuesday, 15 March 2011
One of the new features of C++0x is the ability to capture
exceptions in a std::exception_ptr
and rethrow them later
without knowing what type they are. This is particularly
useful, as it allows you to propagate the exceptions across threads
— you capture the exception in one thread, then pass the
std::exception_ptr
object across to the other thread, and
then use std::rethrow_exception()
on that other thread to
rethrow it. In fact, the exception propagation facilities
of std::async
, std::promise
and std::packaged_task
are build on this feature.
To copy or not to copy
The original proposal for the feature required that the exception
was copied when it was captured
with std::current_exception
, but under pressure from
implementors that use
the "Itanium
ABI" (which is actually used for other platforms too, such as
64-bit x86 linux and MacOSX), the requirement was lessened to allow
reference counting the exceptions instead. The problem they cited
was that the ABI didn't store the copy constructor for exception
objects, so when you called std::current_exception()
the information required to copy the object was not present.
Race conditions
Unfortunately, no-one foresaw that this would open the door for race conditions, since the same exception object would be active on multiple threads at once. If any of the thread modified the object then there would be a data race, and undefined behaviour.
It is a common idiom to catch exceptions by non-const reference in
order to add further information to the exception, and then rethrow
it. If this exception came from another thread (e.g. through use
of std::async
), then it may be active in multiple
threads at once if it was propagated
using std::shared_future
, or even just
with std::exception_ptr
directly. Modifying the
exception to add the additional information is thus a data race if
done unsynchronized, but even if you add a mutex to the class you're
still modifying an exception being used by another thread, which is
just wrong.
Race conditions are bad enough, but these race conditions are implementation-dependent. The draft allows for the exceptions to be copied (as originally intended), and some compilers do that (e.g. MSVC 2010), and it also allows for them to be reference counted, and other compilers do that (e.g. gcc 4.5 on linux). This means that code that is well-defined and race-condition-free on MSVC 2010 might be buggy, and have data races when compiled with gcc 4.5, but the compiler will not (and cannot) warn about it. This is the nastiest of circumstances — race conditions silently added to working code with no warning.
Dealing with the issue
BSI raised an issue on the FCD about this when it came to ballot time. This issue is GB-74, and thus must be dealt with one way or the other before the C++0x standard is published (though sadly, "being dealt with" can mean that it is rejected). This is being dealt with by LWG, so is also listed as LWG issue 1369, where there is a more complete proposed resolution. Unfortunately, we still need to convince the rest of the committee to make this change, including those implementors who use the Itanium ABI.
Extending the ABI
Fortunately, the Itanium ABI is designed to be extensible in a
backwards-compatible manner. This means that existing code compiled
with an existing compiler can be linked against new code compiled
with a new compiler, and everything "just works". The old code only
uses the facilities that existed when it was written, and the new
code takes advantage of the new facilities. This means that the
exception structures can be enhanced to add the necessary
information for copying the exception (the size of the object, so
we can allocate memory for it, and the address copy constructor, or
a flag to say "use memcpy
".) This isn't quite perfect,
as exceptions thrown from old code won't have the new information,
but provided we can detect that scenario all is well, as the
standard draft allows us to throw std::bad_exception
in that case.
I have written a patch for gcc 4.5.0 which demonstrates this as a proof of concept.
This patch extends the exception structures as allowed by the ABI
to add two fields: one for the object size, and one for the copy
constructor. Exceptions thrown by old code will have a size of zero
(which is illegal, so acts as a flag for old code), and thus will be
captures as std::bad_exception
when stored in
a std::exception_ptr
. Trivially copyable objects such
as a plain int
, or a POD class
will have
a NULL
copy constructor pointer but a valid size,
indicating that they are to be copied using memcpy
.
To use the patch, get the sources for gcc 4.5.0 from your
local GCC mirror,
unpack them, and apply the patch. Then compile the patched sources
and install your new gcc somewhere. Now, when you compile code that
throws exceptions it will use the
new __cxa_throw_copyable
function in place of the
old __cxa_throw
function to store the requisite
information. Unless you link against the right support code then
your applications won't link; I found I had to use
the -static
command line option to force the use of the
new exception-handling runtime rather than the standard platform
runtime.
/opt/newgcc/bin/g++ -std=c++0x -static foo.cpp -o foo
Note that if the copy constructor of the exception is not thread
safe then there might still be an issue when
using std::exception_ptr
, as my patch doesn't include a
mutex. However, this would be an easy extension to make now the
proof of concept has been done. I also expect that there are cases
that don't behave quite right, as I am far from being an expert on
the gcc internals.
Hopefully, this proof of concept will help convince the rest of the C++ committee to accept GB-74 at the meeting in Madrid next week.
Posted by Anthony Williams
[/ cplusplus /] permanent link
Tags: C++, C++0x, WG21, Exceptions, Copying, exception_ptr, rethrow_exception
Stumble It! | Submit to Reddit | Submit to DZone
If you liked this post, why not subscribe to the RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.
Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy
5 Comments
This is really amazing place where you can easily acquire moviestarplanet vip hack online within minute. Msp star coins and diamonds also available on this website.
Anthony,
Fantastic of you to do go ahead and patch gcc like that, I also hope this proof of concept helps convince everyone involved.
Now I just hope that your book will stop slipping its ship date on my amazon pre-order! :P
Great work! Could post the patch as a unified diff (diff -u) to make it a bit easier to read? Thanks for trying to tackle this.
@Anon: Just for you, I've posted a diff -u at http://www.justsoftwaresolutions.co.uk/files/copy_exceptions-u.patch
As you have not updated this post, I will mention that the C++ committee rejected the BSI motion by a large majority. Without recapping all of the arguments here, I will highlight just a couple of the points against: 1) For a number of reasons, Anthony's extension to the Itanium ABI was considered insufficient (thanks for trying -- it helped to have a proof of concept, even though it did not ultimately carry the day). As a result, at least one major vendor (Apple) and any number of smaller vendors would not implement the new requirement for at least 6 years IF EVER. 2) A copy requirement adds overhead to every use of exception_ptr, even when no race is present. 3) Any protection is limited to non-pointer types. If you throw a pointer or an object with a shared_ptr in it, you can have races even if you copy the root exception. 4) We cannot hide multitasking headaches from the user. In my words: Serial code can be oblivious to threads. However, the code that *uses* a threading facility (like shared_future) is responsible for hiding the details from the serial code that it calls and the serial code that calls it. "There be dragons here!" It's the dragon-slayer's job to protect the villagers. The user of a threading facility is responsible for catching any exception coming out of serial code, copying it (if necessary) and rethrowing it in a form that is appropriate for the serial client. Don't ask the C++ runtime system to guess at the correct behavior. One last thing: I agree with Anthony that the current situation is the worst of all worlds -- with permission, but no requirement, to copy. We should make sure to fix that in the next revision of the standard (or better yet, in a defect report).