c++ c++11 language-lawyer destructor noexcept

c++> class constructor



¿Por qué std:: unique_ptr:: reset() siempre es noexcept? (4)

Los requisitos de la llamada al objeto de función Deleter son específicos en este caso, como se Deleter en los requisitos del std::unique_ptr<T>::reset() .

De [unique.ptr.single.modifiers]/3 , circa N4660 §23.11.1.2.5 / 3;

modificadores unique_ptr

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

Requiere: La expresión get_deleter()(get()) estará bien formada, tendrá un comportamiento bien definido y no arrojará excepciones .

En general, el tipo debería ser destructible. Y de acuerdo con la cppreference en el concepto Destructible de C ++, el estándar lo enumera debajo de la tabla en [utility.arg.requirements]/2 requisitos de utilidad.arg./2, §20.5.3.1 (énfasis mío);

Requisitos Destructible

u.~T() Todos los recursos que u posee se reclaman, no se propaga ninguna excepción .

También tenga en cuenta los requisitos generales de la biblioteca para las funciones de reemplazo; [res.on.functions]/2 .

Una pregunta reciente (y especialmente mi respuesta) me hizo preguntar:

En C ++ 11 (y estándares más nuevos), los destructores siempre son implícitamente noexcept , a menos que se especifique lo contrario (es decir, noexcept(false) ). En ese caso, estos destructores pueden lanzar excepciones legalmente. (Tenga en cuenta que esto sigue siendo un debería saber realmente lo que está haciendo, ¡ tipo de situación!)

Sin embargo, todas las sobrecargas de std::unique_ptr<T>::reset() se declaran siempre noexcept (vea cppreference ), incluso si el destructor no es T , lo que da como resultado la finalización del programa si un destructor arroja una excepción durante el reset() . Se aplican cosas similares a std::shared_ptr<T>::reset() .

¿Por qué reset() siempre es noexcept, y no condicionalmente noexcept?

Debería ser posible declararlo noexcept(noexcept(std::declval<T>().~T())) que hace que no exista exactamente si el destructor de T es noexcept. ¿Me estoy perdiendo algo aquí, o se trata de un descuido en el estándar (ya que esta es sin dudas una situación altamente académica)?


Quizás esto sería más fácil de explicar esto con un ejemplo. Si suponemos que el reset no siempre fue noexcept , entonces podríamos escribir un código como este, que causaría problemas:

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); }

¿Qué hacemos cuando se p.reset(b) ?

Queremos evitar fugas de memoria, por lo que p necesita reclamar la propiedad de b para que pueda destruir la instancia, pero también necesita destruir a que quiera lanzar una excepción. Entonces, ¿cómo y destruimos tanto a como b ?

Además, ¿qué excepción debería doStuff() ? bad_exception o invalid_argument ?

Forzar el reset para que siempre sea noexcept sin noexcept previene estos problemas. Pero este tipo de código sería rechazado en tiempo de compilación.


Sin haber estado en las discusiones en el comité de estándares, mi primer pensamiento es que este es un caso en el que el comité de estándares ha decidido que el dolor de arrojar el destructor, que generalmente se considera comportamiento indefinido debido a la destrucción de la memoria de la pila cuando se desenrolla la pila, no valió la pena.

Para unique_ptr en particular, considere lo que podría suceder si un objeto sostenido por un unique_ptr arroja el destructor:

  1. Se unique_ptr::reset() .
  2. El objeto dentro se destruye
  3. El destructor arroja
  4. La pila comienza a desenrollarse
  5. El unique_ptr sale del alcance
  6. Ir a 2

Había formas de evitar esto. Uno de ellos es establecer el puntero dentro de unique_ptr a nullptr antes de eliminarlo, lo que daría lugar a una pérdida de memoria, o definir lo que debería suceder si un destructor arroja una excepción en el caso general.


std::unique_ptr::reset no invoca el destructor directamente, sino que invoca el operator () del parámetro de plantilla del eliminador (que por defecto es std::default_delete<T> ). Se requiere que este operador no genere excepciones, como se especifica en

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

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

Requiere: La expresión get_deleter()(get()) estará bien formada, tendrá un comportamiento bien definido y no arrojará excepciones.

Tenga en cuenta que no throw no es lo mismo que noexcept embargo. operator () de default_delete no se declara como noexcept aunque solo invoque el operador delete (ejecuta delete statement). Así que este parece ser un lugar bastante débil en el estándar. reset debe ser condicionalmente noexcept:

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

o al operator () del eliminador se le debe exigir que sea no noexcept para dar una garantía Stonger.