MSVC can’t handle move-only exception types

Here’s my second post from C++Now 2019!

On Friday morning, Andreas Weis gave a very good summary of how exception handling works in C++. I particularly liked that he didn’t just focus on the Itanium ABI (as I would certainly have been tempted to do), but showed in-depth knowledge of how exception handling works on MSVC and also on embedded platforms. The only thing that could have made Andreas’s talk better would have been if he’d mentioned my dynamic_cast From Scratch” (CppCon 2017). :)

Andreas discussed the different ways an implementation could allocate memory for a thrown exception. The familiar way (if you’re an Itanium ABI chauvinist like me) is that the code calls __cxa_allocate_exception, which is basically malloc, so all your in-flight exception objects go on the heap all the time. Andreas mentioned the “emergency buffer” set aside so that even when malloc is exhausted, you’ll be able to throw bad_alloc. But he also mentioned that on MSVC, sometimes (always?), the exception object is initially allocated on the stack!

That is, MSVC will allocate the in-flight exception object into the thrower’s own stack frame. Then, when the appropriate catch block is found, the catch handler will execute with its stack pointer pointing even further below the start of the thrower’s stack frame (almost as if the site of the throw were instead a call to a made-up function consisting of the catch handler code). When the catch-handler “returns,” the stack pointer flies all the way back up to the catching function’s original level.

Destructors for stack variables in between the thrower and the catcher are still correctly destroyed, prior to executing the catch handler.

But if you allocate the exception object on the stack, how do you implement std::exception_ptr, which is supposed to extend the lifetime of a (heap-allocated) exception object and allow it to be passed even between threads? (exception_ptr is basically a shared_ptr with a very curtailed public interface.) You can’t have an exception_ptr pointing into the stack — as soon as you left the last catch handle and the stack pointer flew back up, you’d quickly trash the stack slot you were using to store the exception object.

So what MSVC does is, when you construct an exception_ptr, it copies the exception object from the stack to the heap. From that point on, it can proceed similarly to the Itanium ABI with its heap-allocated exception objects. You can observe this happening on rextester:

#include <exception>
#include <stdio.h>

struct Widget {
    Widget() { puts("constructing the exception object of type Widget on the stack"); }
    Widget(const Widget&) { puts("copying the Widget object from the stack to the heap"); }
    ~Widget() { puts("destroying a Widget object"); }
};

std::exception_ptr ex;

void bar() {
    puts("Now throwing Widget...");
    throw Widget();
}

void foo() {
    try {
        bar();
    } catch (...) {
        puts("Found the catch handler.");
        puts("Now calling current_exception...");
        ex = std::current_exception();
    }
}

int main() {
    foo();
    puts("Now setting ex to nullptr...");
    ex = nullptr;
    puts("Now exiting main()");
}

This program prints

Now throwing Widget...
constructing the exception object of type Widget on the stack
Found the catch handler.
Now calling current_exception...
copying the Widget object from the stack to the heap
destroying a Widget object
Now setting ex to nullptr...
destroying a Widget object
Now exiting main()

Compare to the Itanium ABI (Linux or OSX), where it prints

Now throwing Widget...
constructing the exception object of type Widget on the stack
Found the catch handler.
Now calling current_exception...
Now setting ex to nullptr...
destroying a Widget object
Now exiting main()

On the Itanium ABI, my output messages are a lie: we don’t construct the exception object on the stack, but rather store it on the heap from the get-go. Only MSVC does this weird “construct it on the stack and then move it to the heap” business.

Finally, when you call std::rethrow_exception(ex) on MSVC, it seems to copy the exception object back, from the heap to the stack — which strikes me as surprisingly wasteful, but might be necessary for backward compatibility with catch handlers who expect the exception object to be stack-allocated.


Notice that even though we wrote catch (...), MSVC still knows how to call the copy constructor of Widget and how to call the destructor of Widget. Those two pieces of information are held in MSVC’s exception-handling data structures (essentially a form of type erasure).

However, what if we were to throw an exception object that was not copyable at all? Well, technically, that would be ill-formed. [except.throw/5]:

When the thrown object is a class object, the constructor selected for the copy-initialization as well as the constructor selected for a copy-initialization considering the thrown object as an lvalue shall be non-deleted and accessible, even if the copy/move operation is elided.

In other words, it is ill-formed to throw a move-only type such as unique_ptr! No compiler vendor implements this rule. For the non-MSVC ones who allocate exception objects on the heap and never copy them, the rule serves no purpose. For MSVC… well, let’s see. (Rextester.)

struct Copyable {
    Copyable() { puts(" creating"); }
    Copyable(const Copyable&) { puts(" copying"); }
    ~Copyable() { puts(" destroying"); }
};

std::exception_ptr ex;

int main()
{
    try {
        puts("Throwing an exception of type Copyable...");
        throw Copyable();
    } catch (...) {
        puts("Found the catch block.");
        puts("Now calling current_exception...");
        ex = std::current_exception();
        try {
            puts("Now calling rethrow_exception...");
            std::rethrow_exception(ex);
        } catch (...) {
            puts("Found the next catch block.");
        }
    }

    puts("Now setting ex to nullptr...");
    ex = nullptr;
    puts("Now exiting main()");
}

This program prints

Throwing an exception of type Copyable...
 creating
Found the catch block.
Now calling current_exception...
 copying
Now calling rethrow_exception...
 copying
Found the next catch block.
 destroying
 destroying
Now setting ex to nullptr...
 destroying
Now exiting main()

But replace Copyable with MoveOnly:

struct MoveOnly {
    MoveOnly() { puts(" creating"); }
    MoveOnly(MoveOnly&&) { puts(" moving"); }
    ~MoveOnly() { puts(" destroying"); }
};

Now it prints

Throwing an exception of type MoveOnly...
 creating
Found the catch block.
Now calling current_exception...
Now calling rethrow_exception...
Found the next catch block.
 destroying
 destroying
Now setting ex to nullptr...
 destroying
Now exiting main()

That is, MSVC’s runtime seems to assume that when an exception type has no copy constructor (or a deleted copy constructor), then it should use memcpy to “copy” the exception object from the stack to the heap and back!

Yet, also notice, the runtime still believes it knows how to destroy objects of type MoveOnly. Therefore we get three calls to the destructor (destroying the original stack object, the heap-allocated copy, and the rethrown stack copy), matching up to only one call of the constructor.

Conclusion

If you throw std::make_unique<int>(); on MSVC, and then traffic it through an exception_ptr, you’ll get a double- or triple-delete of the controlled pointer. And since exception_ptr is used internally by STL facilities such as std::future and std::async, you really can’t be sure when you’re using exception_ptr and when you’re not.

Example:

class danger : public std::runtime_error {
    using runtime_error::runtime_error;
    std::unique_ptr<int> state_ = std::make_unique<int>();
};

int main() {
    auto f = std::async([]() {
        throw danger("hello");
    });
    try {
        f.get();
    } catch (...) {
        std::cout << "Double-delete incoming!" << std::endl;
    }
}

The above program is ill-formed (by [except.throw]/5). But every vendor will compile it successfully. And every vendor will run it successfully — except for MSVC. MSVC will triple-delete the state_ member of the exception object (once from the stack, and then again from the heap, and then again from the stack), leading to heap corruption and crash.


When I heard about this in Andreas’s talk, initially I thought, “Hey, here’s another application for trivial relocatability!” MSVC is basically assuming it’s okay to memcpy these objects around, which sounds related to P1144. But notice that MSVC is merely replacing the move constructor with memcpy — they’re still explicitly calling the destructor on the moved-from object. So this is not an application of the P1144 “trivially relocatable” concept. Instead, the practical requirement on exception types in MSVC is

template<class Ex>
struct is_exception_type : std::bool_constant<
    std::is_copy_constructible_v<Ex> ||
    (std::is_trivially_move_constructible_v<Ex> && std::is_trivially_destructible_v<Ex>)
> {};
Posted 2019-05-11