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 queu
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:
- Se
unique_ptr::reset()
. - El objeto dentro se destruye
- El destructor arroja
- La pila comienza a desenrollarse
- El
unique_ptr
sale del alcance - 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.