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

33

最近的一个问题(尤其是我的回答)让我想起了一个问题:

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

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

为什么reset()始终是noexcept,而不是有条件的noexcept

它应该可以声明为noexcept(noexcept(std::declval<T>().~T())),这样它就只有在T的析构函数是noexcept时才是noexcept的。我在这里错过了什么,还是这是标准中的一个疏漏(因为这显然是一个高度学术的情况)?

4个回答

26
调用函数对象Deleter的要求在std::unique_ptr::reset()成员的要求中列出,如Deleter所示。
来自于[unique.ptr.single.modifiers]/3,大约N4660 §23.11.1.2.5/3;
“unique_ptr”修饰符 void reset(pointer p = pointer()) noexcept; 要求:表达式get_deleter()(get())应该是良好定义的,具有明确定义的行为,并且不应抛出异常。
通常情况下,类型需要可析构。根据C++概念“可析构”的cppreference,标准将其列在[utility.arg.requirements]/2,§20.5.3.1表格中(我强调);
“Destructible”需求 u.~T()所有由u拥有的资源都被回收,不会传播任何异常。
还请注意替换函数的一般库要求;[res.on.functions]/2

2
但是标准要求T必须是可销毁的吗?我找不到相关信息。 - el.pescado - нет войне
2
对于使用默认的“Deleter”,它会起作用。自定义的“Deleter”可能能够绕过,但是“Deleter”对象的调用仍然需要不传播任何异常。 - Niall
1
通常来说,标准库容器不支持具有抛出析构函数的类型。如果unique_ptr::reset可能会抛出异常,那么unique_ptr::~unique_ptr也可能会抛出异常,然后std::vector<std::unique_ptr>就不符合标准了,其他所有容器也是如此。这是一个相当糟糕的情况,而且对于使用抛出析构函数的兴趣非常小,因此在标准库中支持它的兴趣也很小。 - Chris Beck
1
这个也很好引用,并且更加普遍适用:http://eel.is/c++draft/res.on.functions#2 - Deduplicator
@ChrisBeck,那很有道理。 - el.pescado - нет войне

6

std::unique_ptr::reset不会直接调用析构函数,而是调用删除器模板参数的operator ()(默认为std::default_delete<T>)。根据规定,此操作符必须不抛出异常。

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

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

要求:表达式get_deleter()(get())应良好定义且无异常抛出。

需要注意的是,“应无异常抛出”与noexcept并不相同。即使default_deleteoperator ()仅调用delete运算符(执行delete语句),它仍未声明为noexcept。因此,这似乎是标准中的一个弱点。reset应该是有条件的noexcept

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

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


3
operator delete 不会调用析构函数。delete 表达式会先调用析构函数,然后再调用 operator delete - M.M
@DavidHammen 我认为有一个很大的误解。delete p; 可以被称为 删除表达式 或者 删除操作调用(因为 delete 在这里被用作运算符)。而 operator delete(p); 可以被称为 operator delete 函数调用。我没有在任何地方提到 operator delete 函数调用。 - user7860670
1
@VTT:标准既不能保证也不需要 delete p; 不抛出异常。它也既不能保证也不需要 operator delete(p) 不抛出异常。然而,对于 std::unique_ptr,它要求 get_deleter()(get()) 是良好形式化、有明确定义的行为,并且不会抛出异常。在 std::unique_ptr 中使用的要求比较严格,要比更松散、更通用的要求更强。这里没有矛盾。 - David Hammen
@VTT - std::default_delete::operator()没有标记为noexcept - David Hammen
@DavidHammen 这正是我所说的。 - user7860670
显示剩余9条评论

4

在没有参加标准委员会的讨论的情况下,我的第一反应是,标准委员会已经决定不值得为了析构函数而付出代价。由于堆栈内存在解开堆栈时被销毁,因此析构函数通常被认为是未定义行为。

特别是对于unique_ptr,考虑一下如果一个对象在析构函数中抛出异常会发生什么:

  1. 调用unique_ptr::reset()
  2. 内部的对象被销毁
  3. 析构函数抛出异常
  4. 堆栈开始解开
  5. unique_ptr超出范围
  6. 返回2

有两种避免这种情况的方法。一种是在删除之前将unique_ptr中的指针设置为nullptr,这将导致内存泄漏;另一种是定义在一般情况下析构函数抛出异常时应该发生什么。


#5不会发生:展开继续从抛出unique_ptr之后的点继续。如果您找到另一个抛出异常的析构函数,那么将调用std::terminate - Davis Herring

0
也许通过一个例子来解释会更容易理解。如果我们假设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_exception还是invalid_argument

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


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