【问题标题】:Moveable but Non-Copyable Exceptions可移动但不可复制的异常
【发布时间】:2012-10-10 16:32:00
【问题描述】:

我正在考虑编写不可复制的异常类。我觉得这很有趣,因为这样我就不必担心在复制构造函数中分配期间可能引发的异常。如果异常对象创建成功,则一切正常,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_exceptionstd::current_exception。两个编译器都接受这个版本。这应该清楚地表明,如果编译器在抛出异常时不需要复制构造函数,那么在使用这两个函数时编译器将不需要。

【问题讨论】:

  • 您多久遇到一次异常对象抛出异常并导致终止的问题?
  • 15.1/5 写道:“当抛出的对象是类对象时,复制/移动构造函数和析构函数应该是可访问的,即使复制/移动操作被省略。”
  • 我只是想知道删除的函数是否被认为是可访问的(您的函数是公共的)。 8.4.3 没有提到任何关于此的内容。
  • @KerrekSB:我不知道异常类的抛出复制构造函数曾经终止我的一个程序。但是,我过去写过一些服务,它们必须在内存不足的情况下工作。有一些准则建议不要从异常的类复制构造函数中抛出异常,例如securecoding.cert.org/confluence/display/cplusplus/…

标签: c++ exception c++11


【解决方案1】:

current_exception 说它指的是当前异常或它的副本,但没有说是哪个。这向我表明:

  • 未指定是否复制[*]
  • 因此,您的异常类不好(如果有人可能会在上面调用current_exception,那肯定不行)
  • 因此,它在某些实现中起作用也就不足为奇了。除非实施者对此有要求或希望实施者避免复制,否则可能不会对当前例外进行复制。

只是扔东西并通过引用抓住它就可以了。 throw 表达式中的临时值是“用于初始化”实现所使用的对象以保持当前异常,因此可以移动或复制它(根据类支持),并且可以省略移动/复制.

不管怎样,make_exception_ptr 被指定为总是复制。您可能会争辩说这是一个缺陷:e 可以是一个按值参数,在这种情况下移动可能会更好。但这是我的浮躁和无知的印象,我以前从未见过这些功能。

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

【讨论】:

    【解决方案2】:

    但是,我有点惊讶。据我所知,有几种情况可能会复制异常对象,例如std::current_exception()std::rethrow_exception()

    但你没有打电话给他们。该标准非常清楚异常对象是如何初始化的。从 15.1 开始,p3:

    throw-expression 初始化一个临时对象,称为异常对象,其类型通过从 throw 操作数的静态类型中移除任何顶级 cv-qualifier 并从“array of T”或“函数返回 T”分别指向“指向 T 的指针”或“指向返回 T 的函数的指针”。临时值是一个左值,用于初始化匹配处理程序 (15.3) 中命名的变量。如果异常对象的类型是不完整类型或指向不完整类型的指针(可能是 cv 限定的)void,则程序格式错误。除了这些限制和 15.3 中提到的类型匹配限制之外,throw 的操作数在 a 调用 (5.2.2) 或返回语句的操作数。

    简而言之,它就像按值返回类型:返回值/异常对象由您提供的表达式初始化。因为你使用的表达式是一个临时的,它的行为就像从一个函数中返回一个临时的并调用移动构造函数。诚然,这很可能会被忽略,但这就是 15.1, p5 的意义所在:

    当抛出的对象是类对象时,复制/移动构造函数和析构函数应该是可访问的,即使复制/移动操作被省略(12.8)。

    这对于返回值也是如此:在适当的情况下,返回值通过复制/移动初始化进行初始化。因此,适当的构造函数必须是可访问的,即使它们被省略了。

    您不能以需要复制构造异常对象的方式抛出异常类。所以你不能抛出左值;你只能抛出一个prvalue或一个xvalue。

    标准中没有任何地方说允许系统任意无缘无故地复制异常。调用std::current_exception 可以复制它。调用std::rethrow_exception 可能会复制它。

    但如果你不调用显式复制你的异常对象的东西,C++不允许这样做。

    【讨论】:

    • 对不起,我不同意你的看法。 std::current_exception 使用 std::exception_ptr - 不是任何具体的异常类。编译器复制异常的唯一方法需要在抛出异常的地方使用魔法。
    • @nosid:你错过了重点:抛出异常时发生的任何“魔术”与是否需要调用复制构造函数无关。该标准没有说您抛出的异常将被复制,因此它不需要复制构造函数。只有调用 current_exception 和其他类似函数才会强制使用复制构造函数。
    • 我认为你错过了重点:如果你用void foobar() { return exception{}; } 替换主函数,即具有外部链接的函数,编译器不知道std::current_exception 是否用于我的非可复制的异常。在实际使用std::current_exception 时复制异常的唯一方法是在抛出异常的地方做一些魔术。这并不意味着在抛出点复制异常。但是,编译器可以存储一个指向将执行复制的函数的指针。
    • 我的意思是void foobar() { throw exception{}; }
    • @nosid:无论需要什么魔法才能使current_exception 工作,不允许要求所有异常类型都是可复制的。 C++ 标准不要求抛出的对象是可复制的,除非您复制它们。因此,使这一切工作的魔法必须像允许std::vector 使用不可复制的T 同时仍然拥有push_back 的魔法一样工作,这确实需要T 是可复制的。它必须基于复制异常对象的尝试,而不是抛出它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-28
    • 2023-03-07
    • 1970-01-01
    • 2012-12-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多