Почему 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
бросает в деструктор:
-
unique_ptr::reset()
. - Объект внутри разрушен
- Броски деструктора
- Стек начинает раскручивать
-
unique_ptr
выходит за рамки - Перейти к 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
предотвращает эти проблемы. Но такой код будет отклонен во время компиляции.