为什么 std::unique_ptr::reset() 总是 noexcept?

Why is std::unique_ptr::reset() always noexcept?

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

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

但是,所有重载 std::unique_ptr<T>::reset() 被声明为总是 noexcept(参见 cppreference),即使析构函数 if T 不是,如果析构函数在 reset()。类似的事情适用于 std::shared_ptr<T>::reset().

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

应该可以声明它 noexcept(noexcept(std::declval<T>().~T())) 如果 T 的析构函数是 noexcept 就可以使它成为 noexcept。我是不是遗漏了什么,或者这是对标准的疏忽(因为公认这是一个高度学术化的情况)?

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

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

unique_ptr modifiers

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

Requires: The expression get_deleter()(get()) shall be well formed, shall have well-defined behavior, and shall not throw exceptions.

一般来说,类型需要是可破坏的。根据 cppreference on the C++ concept Destructible, the standard lists this under the table in [utility.arg.requirements]/2,§20.5.3.1(强调我的);

Destructible requirements

u.~T() All resources owned by u are reclaimed, no exception is propagated.

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

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

中指定

23.11.1.2.5 unique_ptr modifiers [unique.ptr.single.modifiers]

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

Requires: The expression get_deleter()(get()) shall be well-formed, shall have >well-defined behavior, and shall not throw exceptions.

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

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

operator ()的删除者应该被要求noexcept以提供更强的保证。

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

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

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

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

也许用一个例子来解释会更容易。如果我们假设 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 可以防止这些问题。但是这种代码会在 compile-time.

处被拒绝