Почему std :: unique_ptr :: reset() всегда noexcept?

Недавний вопрос (и особенно мой ответ) заставлял меня задаться вопросом:

В С++ 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 является исключением. Я что-то пропустил здесь, или это надзор в стандарте (поскольку это, по общему признанию, очень академическая ситуация)?

Ответы

Ответ 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()) должно быть хорошо сформировано, должно иметь четко определенное поведение и не должно генерировать исключения.

В общем, тип должен быть разрушаемым. И согласно cppreference по концепции C++ Destructible, стандарт перечисляет это под таблицей в [utility.arg.requirements]/2, §20.5.3.1 (выделение мое);

Destructible требования

u.~T() Все ресурсы, принадлежащие u, восстанавливаются, никакое исключение не распространяется.

Также обратите внимание на общие требования к библиотеке для функций замены; [res.on.functions]/2.

Ответ 2

std::unique_ptr::reset не вызывает непосредственно деструктор, вместо этого он вызывает operator() параметра шаблона deleter (который по умолчанию соответствует 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() default_delete не объявляется как noexcept хотя он вызывает только оператор delete (выполняет команду delete). Так что это кажется довольно слабым местом в стандарте. reset должен либо быть условно noexcept:

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

или operator() отладчика должны быть не за noexcept чтобы дать более строгую гарантию.

Ответ 3

Не будучи в дискуссиях в комитете по стандартизации, моя первая мысль заключается в том, что это тот случай, когда комитет по стандартам решил, что боль в метании деструктора, которая обычно считается неопределенным поведением из-за разрушения памяти стека при разматывании стек, не стоило того.

Для unique_ptr в частности, рассмотрим, что может произойти, если объект, содержащий unique_ptr бросает в деструктор:

  1. unique_ptr::reset().
  2. Объект внутри разрушен
  3. Броски деструктора
  4. Стек начинает раскручивать
  5. unique_ptr выходит за рамки
  6. Перейти к 2

Были способы избежать этого. Один из них устанавливает указатель внутри unique_ptr на nullptr перед его удалением, что приведет к утечке памяти или определению того, что должно произойти, если деструктор генерирует исключение в общем случае.

Ответ 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 Итак, как и мы уничтожаем как a и b?

Кроме того, какое исключение должно doStuff() throw? bad_exception или invalid_argument?

Принудительный reset всегда должен быть noexcept предотвращает эти проблемы. Но такой код будет отклонен во время компиляции.