抛出异常时不调用移动构造函数

Move constructor is not called when throwing an exception

我有一个变量,它累积当前异常并且需要在抛出当前异常时进行清理(这样就不会再次报告相同的错误)。问题是 throw std::move(ex); 不调用移动构造函数(这会清除 ex),而是调用复制构造函数(这样 ex 也会保留已经抛出的错误)。 MVCE 如下:

#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;
}

我正在使用 MSVC++2013 更新 5。

是不是我做错了什么导致移动构造函数没有被调用?有没有办法抛出异常,使得C++中用于异常存储的临时对象是移动构造的,而不是从原始对象复制构造的?

我尽量避免的是双重复制:构建tmt1的副本,然后清理原始副本,然后在throw语句中使用副本,这将构造另一个临时副本存储。

编辑:上面的代码示例在 MSVC++2013 Update 5 上给出了以下输出

Copy
Caught Test1

而预期的输出是

Move
Caught Test1

EDIT2:提交编译器错误报告https://connect.microsoft.com/VisualStudio/feedback/details/1829824

这是一个编译器错误。标准参考 §12.8/p32 指出它应该调用移动构造函数(确认@Piotr Skotnicki)。

这是一个 MSVC 错误。来自 [except.throw]:

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

这意味着我们这样做:

ThrowMoveTest __exception_object = move(tmt1);

绝对应该调用移动构造函数。


请注意,这里的 move 是不必要的,而且也是有害的。 [class.copy]规定copy/move构造可以省略

— 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

所以简单地 throw tmt1; 将允许 tmt1 直接构造到异常对象中。虽然 gcc 和 clang 都没有这样做。

即使 copy/move 没有被省略:

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.

所以throw tmt1;仍然会移动构造异常对象。