可移动但不可复制的异常

3

我正在考虑编写不可复制的异常类。我觉得这很有趣,因为这样我就不必担心复制构造函数中可能抛出的异常。如果异常对象的创建成功,一切都很好,就不应该出现与std::terminate有关的问题。

struct exception
{
    exception() = default;
    exception(const exception&) = delete;
    exception(exception&&) noexcept = default;
    ~exception() noexcept = default;
    auto operator=(const exception&) -> exception& = delete;
    auto operator=(exception&&) noexcept -> exception& = delete;
};

int main()
{
    try {
        try {
            throw exception{};
        } catch (...) {
            std::rethrow_exception(
                std::current_exception());
        }
    } catch (const exception& e) {
        return 1;
    }
}

GCC-4.7和Clang-3.2接受上述代码。但我有点惊讶。据我所知,有几种情况下可能会复制异常对象,例如std::current_exception()和std::rethrow_exception()。
问题:根据C++11标准,上述代码是否正确?是否会被符合C++11标准的所有编译器接受?
编辑:将std::rethrow_exception和std::current_exception添加到示例中。两个编译器都接受这个版本。这应该说明,如果编译器在抛出异常时不需要复制构造函数,则使用这两个函数时也不需要复制构造函数。

3
你有多少次遇到过异常对象抛出异常并导致终止的问题? - Kerrek SB
2
15.1/5 表示:“当抛出的对象是类对象时,即使复制/移动操作被省略,复制/移动构造函数和析构函数也必须是可访问的。” - dyp
我只是想知道一个已删除的函数是否被认为是可访问的(您的函数是公共的)。8.4.3没有提到这一点。 - dyp
1
@KerrekSB:我不知道抛出异常类的复制构造函数曾经终止过我的程序。然而,过去我写过一些服务,必须处理内存不足的情况。有一些指南建议不要从异常类的复制构造函数中抛出异常,例如 https://www.securecoding.cert.org/confluence/display/cplusplus/ERR14-CPP.+Do+not+allow+an+exception+class%27s+copy+constructor+to+throw+exceptions - nosid
2个回答

2
current_exception表示它是当前异常或其副本,但没有说明是哪一个。这让我想到:
  • 未指定是否复制它[*]
  • 因此,你的异常类不好(特别是如果任何人可能在其上调用current_exception
  • 因此,在某些实现中它能够工作并不奇怪。除非实现者要求或希望避免复制,否则可能不会为当前异常提供“不”被复制的选项。

只需将其抛出并通过引用捕获即可。在throw表达式中的临时对象“用于初始化”由实现使用的对象以保持当前异常,因此它可以移动或复制(根据类是否支持),并且该移动/复制可以省略。

值得一提的是,make_exception_ptr始终被指定为复制。也许你可以认为这是一个缺陷:当e是按值传递的参数时,移动可能更好。但这只是我的冲动和无知印象,我甚至从未见过这些函数。

[*]明确未指定是否每次调用current_exception都“创建一个新副本”,但我并不确定这是否意味着它未指定是否在第一次调用时创建新副本。


2
然而,我有点惊讶。据我所知,有几种情况下可能会复制异常对象,例如std::current_exception()和std::rethrow_exception()。
但是你没有调用它们中的任何一个。标准非常清楚地说明了异常对象的初始化方式。从15.1, p3开始:
抛出表达式初始化临时对象,称为异常对象,其类型由从throw的操作数的静态类型中删除任何顶层cv修饰符并将类型从“T的数组”或“返回T的函数”调整为“指向T的指针”或“指向返回T的函数的指针”,分别确定。该临时对象是一个lvalue,并用于初始化匹配处理程序(15.3)中命名的变量。如果异常对象的类型是不完整类型或指向除(可能带有cv限定符的)void之外的不完整类型的指针,则程序是非法的。除了15.3中提到的类型匹配限制和这些限制之外,throw的操作数在调用(5.2.2)中被视为函数参数或return语句的操作数。
简而言之,它的作用类似于通过值返回类型:返回值/异常对象由您提供的表达式初始化。因为您使用的表达式是一个临时变量,所以它将像从函数返回临时变量一样工作并调用移动构造函数。当然,很有可能会省略它,但这就是15.1, p5所指出的:
当抛出的对象是类对象时,即使省略了复制/移动操作(12.8),复制/移动构造函数和析构函数也必须是可访问的。
对于返回值,这同样适用:返回值通过复制/移动初始化进行初始化,如适用。因此,适当的构造函数必须是可访问的,即使它们被省略了。
您不能以需要复制异常对象的方式抛出异常类。因此,您不能抛出lvalue;您只能抛出prvalue或xvalue。
标准中没有任何地方说系统允许任意地复制异常而没有原因。调用std::current_exception可能会复制它。调用std::rethrow_exception可能会复制它。
但是,如果您不调用显式复制异常对象的函数,C++就不允许随意复制它们。

抱歉,我不同意你的观点。std::current_exception使用的是std::exception_ptr而不是任何具体的异常类。编译器复制异常的唯一方法需要在抛出异常的地方进行魔法操作。 - nosid
@nosid:你没有理解重点:当异常被抛出时发生的任何“魔法”都与是否需要调用复制构造函数无关。标准并未规定你抛出的异常将被复制,因此它不需要复制构造函数。只有对current_exception和其他类似函数的调用才会强制使用复制构造函数。 - Nicol Bolas
我认为你没有理解重点:如果你用void foobar() { return exception{}; }替换主函数,也就是一个具有外部链接的函数,编译器无法知道std::current_exception是否被用于我的不可复制异常。唯一在实际使用std::current_exception时复制异常的方法是,在抛出异常的地方进行一些魔法操作。这并不意味着异常在抛出点被复制。然而,编译器可以存储指向将执行复制的函数的指针。 - nosid
我指的是 void foobar() { throw exception{}; } - nosid
@nosid:无论需要多少魔法使current_exception起作用,都不允许要求所有异常类型都是可复制的。C++标准并不要求抛出的对象是可复制的,除非你复制它们。因此,使一切正常工作的魔法必须像使std :: vector使用不可复制的T而仍然具有需要T为可复制的push_back一样。它必须基于尝试复制异常对象,而不是抛出异常。 - Nicol Bolas
@nosid:换句话说,标准并没有规定异常类型必须是可复制的,因此它不必是可复制的 - Nicol Bolas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接