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

时间:2018-02-19 07:49:49

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

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

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

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

为什么std::shared_ptr<T>::reset()总是没有例外,而且条件上没有例外?

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

4 个答案:

答案 0 :(得分:25)

对函数对象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 ++概念 Destructible 上的cppreference,标准在[utility.arg.requirements]/2,§20.5.3.1(强调我的)中的表格下列出了这一点;

  

Destructible要求

     

u.~T() u拥有的所有资源都会被回收,不会传播任何例外

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

答案 1 :(得分:5)

std::unique_ptr::reset不会直接调用析构函数,而是调用deleter模板参数的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())应格式正确,具有明确定义的行为,并且不得抛出异常。

请注意,不会抛出noexcept不同。 operator ()的{​​{1}}未声明为default_delete,即使它只调用noexcept运算符(执行delete语句)。所以这似乎是标准中的一个相当弱点。 delete应该是有条件的noexcept:

reset
删除者的

noexcept(noexcept(::std::declval<D>()(::std::declval<T*>()))) 应被要求operator ()以提供更强的保证。

答案 2 :(得分:4)

如果没有参加标准委员会的讨论,我首先想到的是,标准委员会已经决定投入析构函数的痛苦,这通常被认为是由于堆栈的破坏而导致的未定义行为展开堆栈时的内存,不值得。

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

  1. 调用unique_ptr::reset()
  2. 内部对象被销毁
  3. 析构函数抛出
  4. 堆栈开始展开
  5. unique_ptr超出范围
  6. 转到2
  7. 有办法避免这种情况。一种是在将unique_ptr内部的指针设置为nullptr之前删除它,这会导致内存泄漏,或者定义如果析构函数在一般情况下抛出异常会发生什么。 / p>

答案 3 :(得分: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_exceptioninvalid_argument

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