Just Software Solutions

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: , , , , , ,
Stumble It! stumbleupon logo | Submit to Reddit reddit logo | Submit to DZone dzone logo

Comment on this post

If you liked this post, why not subscribe to the RSS feed RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.

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.

by Moviestarplanet vip hack at 15:00:33 on Monday, 21 January 2019

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

by Michal Mocny at 15:00:33 on Monday, 21 January 2019

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.

by Anon at 15:00:33 on Monday, 21 January 2019

@Anon: Just for you, I've posted a diff -u at http://www.justsoftwaresolutions.co.uk/files/copy_exceptions-u.patch

by Anthony Williams at 15:00:33 on Monday, 21 January 2019

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).

by Pablo Halpern at 15:00:33 on Monday, 21 January 2019

Add your comment

Your name:

Email address:

Your comment:

Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy