【问题标题】:Why is std::unique_ptr::reset() always noexcept?为什么 std::unique_ptr::reset() 总是无异常?
【发布时间】:2018-07-29 10:01:36
【问题描述】:

A recent question(尤其是我的回答)让我想知道:

在 C++11(和更新的标准)中,析构函数总是隐含的noexcept,除非另有说明(即noexcept(false))。在这种情况下,这些析构函数可以合法地抛出异常。 (请注意,这仍然是一种你应该真正知道你在做什么——那种情况!)

但是,所有的重载 std::unique_ptr<T>::reset() 被声明为始终为noexcept(参见cppreference),即使T 的析构函数不是,如果析构函数在reset() 期间抛出异常,则会导致程序终止。类似的事情也适用于std::shared_ptr<T>::reset()

为什么reset()总是noexcept,而不是有条件的noexcept?

如果T 的析构函数是noexcept,则应该可以将其声明为noexcept(noexcept(std::declval<T>().~T())),从而使其成为noexcept。我是否在这里遗漏了什么,或者这是对标准的疏忽(因为这无疑是一个高度学术化的情况)?

【问题讨论】:

    标签: c++ c++11 language-lawyer destructor noexcept


    【解决方案1】:

    函数对象Deleter的调用要求在std::unique_ptr<T>::reset()成员的要求中列出。

    来自[unique.ptr.single.modifiers]/3,大约 N4660 §23.11.1.2.5/3;

    unique_ptr 修饰符

    void reset(pointer p = pointer()) noexcept;

    要求:表达式get_deleter()(get()) 应具有良好的格式,应具有明确定义的行为,并且不应不抛出异常

    一般来说,类型需要是可破坏的。根据 C++ 概念中的cppreferenceDestructible,该标准在[utility.arg.requirements]/2, §20.5.3.1(强调我的)中的表格下列出了这一点;

    Destructible 要求

    u.~T()u 拥有的所有资源都被回收,不传播异常

    还要注意替换函数的一般库要求; [res.on.functions]/2.

    【讨论】:

    • 但是标准要求T 是可破坏的吗?我到处都找不到?
    • 对于使用默认的Deleter,它会。自定义的可能可以绕过,但 Deleter 对象的调用仍然需要不传播任何异常。
    • @el.pescado:一般来说,标准库容器不支持具有 throwing dtors 的类型。如果unique_ptr::reset 可以抛出,那么unique_ptr::~unique_ptr 可以抛出,然后std::vector<std::unique_ptr> 被标准禁止,对于所有其他容器也是如此。这是一个非常糟糕的情况,并且对使用 throwing dtors 真的没什么兴趣,所以在标准库中支持它也没什么兴趣。
    • 这个也好引用,适用范围更广:eel.is/c++draft/res.on.functions#2
    • @ChrisBeck 说得通
    【解决方案2】:

    std::unique_ptr::reset 不直接调用析构函数,而是调用删除器模板参数的operator ()(默认为std::default_delete<T>)。要求此运算符不抛出异常,如

    中所述

    23.11.1.2.5 unique_ptr 修饰符 [unique.ptr.single.modifiers]

    void reset(pointer p = pointer()) noexcept;

    要求:表达式get_deleter()(get()) 应具有良好的格式,应具有>良好定义的行为,并且不应引发异常。

    请注意,shall not thrownoexcept 不同。 default_deleteoperator () 未声明为 noexcept,即使它仅调用 delete 运算符(执行 delete 语句)。所以这似乎是标准中一个相当薄弱的地方。 reset 应该是有条件的 noexcept:

    noexcept(noexcept(::std::declval<D>()(::std::declval<T*>())))
    

    或删除者的operator ()应要求为noexcept,以给予更强的保证。

    【讨论】:

    • operator delete 只是释放内存; reset 还必须在某个时候调用析构函数
    • @M.M 你的意思是把删除操作符作为一个函数调用吗?因为删除操作符调用析构函数。
    • @VTT 我认为它是在 15.4.12.4 中定义的:“析构函数也通过使用删除表达式(8.5.2.5)隐式调用由新表达式分配的构造对象( 8.5.2.4);调用的上下文是删除表达式。”删除操作符不调用析构函数,它是隐式调用的。
    • @VTT operator delete 不调用析构函数。 delete 表达式调用析构函数,然后调用operator delete
    • @VTT:该标准既不保证也不要求delete p; 不抛出异常。它既不保证也不要求operator delete(p) 不抛出异常。然而,对于std::unique_ptr,它确实要求get_deleter()(get()) 格式正确、行为明确且无异常。在std::unique_ptr 中使用的要求比更宽松、更通用的要求更强。这里没有矛盾
    【解决方案3】:

    没有参加标准委员会的讨论,我的第一个想法是,这是标准委员会决定抛出析构函数的痛苦的情况,这通常被认为是由于堆栈的破坏而导致的未定义行为展开堆栈时的内存,不值得。

    特别是对于unique_ptr,考虑如果unique_ptr 持有的对象抛出析构函数会发生什么:

    1. unique_ptr::reset() 被调用。
    2. 里面的对象被破坏了
    3. 析构函数抛出
    4. 堆栈开始展开
    5. unique_ptr 超出范围
    6. 转到 2

    有办法避免这种情况。一种是在删除之前将unique_ptr 内部的指针设置为nullptr,这会导致内存泄漏,或者定义如果析构函数在一般情况下抛出异常会发生什么。

    【讨论】:

    • #5 没有发生:展开从 过去 点继续进行,unique_ptr 抛出。如果你找到另一个抛出的析构函数,那么你调用std::terminate
    【解决方案4】:

    也许用一个例子来解释这一点会更容易。如果我们假设reset 并不总是noexcept,那么我们可以编写一些这样的代码会导致问题:

    class Foobar {
    public:
      ~Foobar()
      {
        // Toggle between two different types of exceptions.
        static bool s = true;
        if(s) throw std::bad_exception();
        else  throw std::invalid_argument("s");
        s = !s;
      }
    };
    
    int doStuff() {
      Foobar* a = new Foobar(); // wants to throw bad_exception.
      Foobar* b = new Foobar(); // wants to throw invalid_argument.
      std::unique_ptr<Foobar> p;
      p.reset(a);
      p.reset(b);
    }
    

    p.reset(b) 被调用时我们会做什么?

    我们要避免内存泄漏,所以p 需要声明b 的所有权以便它可以销毁实例,但它还需要销毁要抛出异常的a。那么我们如何销毁ab

    另外,doStuff() 应该抛出哪个异常? bad_exceptioninvalid_argument?

    强制reset 始终为noexcept 可以防止这些问题。但是这种代码会在编译时被拒绝。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-12-01
      • 2015-11-28
      • 2015-04-14
      • 1970-01-01
      • 2021-05-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多