Move constructor is not called when throwing an ex

2020-04-13 19:57发布

问题:

I have a variable which accumulates the current exception and needs to get cleaned when the current exception gets thrown (so that the same error does not get reported again). The problem is that throw std::move(ex); does not call the move constructor (which would clean ex), but rather calls a copy constructor (so that ex stays with the already thrown errors too). A MVCE follows:

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

class ThrowMoveTest : exception
{
public:
    ThrowMoveTest(const string& what)
    {
        _what = what;
    }
    ThrowMoveTest(const ThrowMoveTest& fellow)
    {
        cout << "Copy " << what() << endl;
        _what = fellow._what;
    }
    ThrowMoveTest(ThrowMoveTest&& fellow)
    {
        cout << "Move " << what() << endl;
        _what = std::move(fellow._what);
    }
    virtual const char* what() const override
    {
        return _what.c_str();
    }
private:
    mutable string _what;
};

int main()
{
    try
    {
        ThrowMoveTest tmt1("Test1");
        throw move(tmt1);
    }
    catch (const ThrowMoveTest& ex)
    {
        cout << "Caught " << ex.what() << endl;
    }
    return 0;
}

I am using MSVC++2013 Update 5.

Is there something that I am doing wrong so that the move constructor does not get called for this reason? Is there away to throw an exception so that the temporary object used for exception storage in C++ gets move-constructed, rather than copy-constructed from the original?

What I try to avoid is double-copying: construction of a copy of tmt1, then cleaning the original, then using the copy in throw statement, which would construct another copy for temporary storage.

EDIT: The above code example gives the following output on MSVC++2013 Update 5

Copy
Caught Test1

While the expected output is

Move
Caught Test1

EDIT2: Submitted a compiler bug report https://connect.microsoft.com/VisualStudio/feedback/details/1829824

回答1:

This is an MSVC bug. From [except.throw]:

Throwing an exception copy-initializes (8.5, 12.8) a temporary object, called the exception object.

That means we do:

ThrowMoveTest __exception_object = move(tmt1);

which should definitely call the move constructor.


Note that the move here is unnecessary and also damaging. [class.copy] stipulates that copy/move construction can be elided

— in a throw-expression (5.17), when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object (15.1) can be omitted by constructing the automatic object directly into the exception object

So simply throw tmt1; would have allowed for tmt1 to be constructed directly into the exception object. Although neither gcc nor clang do this.

And even if the copy/move is not elided:

When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the object to be copied is designated by an lvalue [...] overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.

So throw tmt1; would still move-construct the exception object.



回答2:

This is a compiler bug. Standard ref §12.8/p32 states it should call the move constructor (acknowledge @Piotr Skotnicki).