Pregunta ¿Qué sucede si 'throw' no asigna memoria para objeto de excepción?


A partir del estándar C ++ 11 (15.1.p4):

La memoria para el objeto de excepción se asigna de forma no especificada   manera, excepto como se señala en 3.7.4.1

¿Qué sucede si falla la asignación? ¿Va a arrojar std::bad_alloc ¿en lugar? Llamada std::terminate? No especificado?


32
2017-08-04 03:09


origen


Respuestas:


(proporcionando mi propia respuesta ... Esperaré unos días y, si no hay problemas con eso, lo marcaré como aceptado)

Pasé un tiempo investigando esto y aquí está lo que descubrí:

  • El estándar C ++ no especifica qué va a pasar en este caso
  • Clang y GCC parecen usar C ++ Itanium ABI

Itanimum ABI sugiere usar heap para excepciones:

El almacenamiento es necesario para las excepciones lanzadas. Este almacenamiento debe   persistir mientras se desenrolla la pila, ya que será utilizada por   controlador, y debe ser seguro para subprocesos. El almacenamiento de objetos de excepción   por lo tanto, normalmente se asignará en el montón

...

La memoria será asignada por el __cxa_allocate_exception rutina de la biblioteca de tiempo de ejecución.

Entonces, sí ... lanzar una excepción probablemente implique bloquear mutexes y buscar un bloque de memoria libre. :-(

También menciona esto:

Si __cxa_allocate_exception no puede asignar un objeto de excepción bajo estas restricciones, llama a terminate ()

Sí ... en GCC y Clang "throw myX ();" puede matar tu aplicación y no puedes hacer nada al respecto (tal vez escribir tu propio __cxa_allocate_exception puede ayudar, pero ciertamente no será portátil)

Se pone aún mejor:

3.4.1 Asignación del objeto de excepción

La memoria para un objeto de excepción será asignada por   __cxa_allocate_exception rutina de biblioteca de tiempo de ejecución, con requisitos generales como se describe en la Sección 2.4.2. Si la asignación normal   falla, entonces intentará asignar uno de los buffers de emergencia,   descrito en la Sección 3.3.1, bajo las siguientes limitaciones:

  • El tamaño del objeto de excepción, incluidos los encabezados, es inferior a 1 KB.
  • El hilo actual aún no tiene cuatro búferes.
  • Hay menos de 16 subprocesos adicionales que contienen almacenamientos intermedios, o este hilo   esperará hasta que uno de los otros libere sus buffers antes de adquirir uno.

Sí, tu programa simplemente puede colgar! Las posibilidades de que esto ocurra son pequeñas, necesitarás agotar la memoria y tus hilos necesitan agotar todos los 16 buffers de emergencia e ingresar espera por otro hilo que genere una excepción. Pero si haces cosas con std::current_exception (como encadenar excepciones y pasarlas entre hilos) - no es tan improbable.

Conclusión:

Esta es una deficiencia en el estándar C ++: no puede escribir programas 100% confiables (que usan excepciones). El ejemplo de libro de texto es un servidor que acepta conexiones de clientes y ejecuta tareas enviadas. El enfoque obvio para manejar los problemas sería arrojar una excepción, que desenrollará todo y cerrará la conexión; el resto de los clientes no se verán afectados y el servidor continuará funcionando (incluso en condiciones de poca memoria). Por desgracia, dicho servidor es imposible de escribir en C ++.

Puede afirmar que los sistemas modernos (es decir, Linux) matarán a ese servidor antes de que lleguemos a esta situación de todos modos. Pero (1) no es un argumento; (2) el administrador de memoria puede configurarse para comprometerse; (3) OOM killer no se activará para aplicaciones de 32 bits que se ejecutan en hardware de 64 bits con suficiente memoria (o si la aplicación limita artificialmente la asignación de memoria).

En lo personal, estoy bastante enojado con este descubrimiento: durante muchos años afirmé que mi código maneja la falta de memoria con gracia. Resulta que mentí a mis clientes. :-( También podría comenzar a interceptar la asignación de memoria, llamar std::terminate y tratar todas las funciones relacionadas como noexcept - Esto ciertamente hará mi vida más fácil (en cuanto a la codificación). No es de extrañar que todavía usen Ada para programas de cohetes.


8
2017-08-07 17:46



[intro.compliance] / 2 Aunque esta Norma Internacional solo establece requisitos en las implementaciones de C ++, esos requisitos a menudo son más fáciles de entender si se expresan como requisitos en programas, partes de programas o ejecución de programas. Dichos requisitos tienen el siguiente significado:

(2.1) - Si un programa no contiene violaciones de las reglas en esta Norma Internacional, una implementación conforme deberá, dentro de sus límites de recursos, acepta y ejecuta correctamente ese programa.

Énfasis mío Básicamente, el estándar prevé la imposibilidad de asignar memoria dinámica (y prescribe el comportamiento en este caso), pero no cualquier otro tipo de memoria; y no prescribe de ninguna manera lo que la implementación debería hacer cuando se alcanzan sus límites de recursos.

Otro ejemplo se está quedando sin pila debido a una recursión demasiado profunda. En ninguna parte el estándar dice qué tan profunda está permitida una recursión. El desbordamiento de pila resultante es la implementación que ejerce su derecho de "dentro de los límites de los recursos".


2
2017-08-05 14:25



La respuesta actual ya describe lo que hace GCC. He comprobado el comportamiento de MSVC: asigna una excepción en la pila, por lo que la asignación no depende del montón. Esto hace que el desbordamiento de pila sea posible (el objeto de excepción puede ser grande), pero el manejo de desbordamiento de pila no está cubierto por C ++ estándar.

Usé este programa corto para examinar lo que ocurre durante el lanzamiento de excepción:

#include <iostream>

class A {
public:
    A() { std::cout << "A::A() at " << static_cast<void *>(this) << std::endl; }
    A(const A &) { std::cout << "A::A(const A &) at " << static_cast<void *>(this) << std::endl; }
    A(A &&) { std::cout << "A::A(A &&) at " << static_cast<void *>(this) << std::endl; }
    ~A() { std::cout << "A::~A() at " << static_cast<void *>(this) << std::endl; }
    A &operator=(const A &) = delete;
    A &operator=(A &&) = delete;
};

int main()
{
    try {
        try {
            try {
                A a;
                throw a;
            } catch (const A &ex) {
                throw;
            }
        } catch (const A &ex) {
            throw;
        }
    } catch (const A &ex) {
    }
}

Cuando build con salida GCC muestra claramente que la excepción lanzada se está asignando lejos de la pila:

A::A() at 0x22cad7
A::A(A &&) at 0x600020510
A::~A() at 0x22cad7
A::~A() at 0x600020510

Cuando build con salida MSVC muestra que la excepción se asigna cerca de la pila:

A::A() at 000000000018F4E4
A::A(A &&) at 000000000018F624
A::~A() at 000000000018F4E4
A::~A() at 000000000018F624

El examen adicional con depurador muestra que los manejadores de captura y los destructores se ejecutan en la parte superior de la pila, por lo que el consumo de pila crece con cada bloque de captura comenzando con el primer lanzamiento y hasta std::uncaught_exceptions() se convierte en 0.

Tal comportamiento significa que el correcto manejo de la falta de memoria requiere que demuestre que hay suficiente espacio de pila para que el programa ejecute manejadores de excepciones y todos los destructores en el camino.

Para demostrar lo mismo con GCC, parece que tendrá que demostrar que no hay más de cuatro excepciones anidadas y las excepciones tienen un tamaño inferior a 1 KB (esto incluye el encabezado). Además, si un hilo tiene más de cuatro excepciones anidadas, también debe demostrar que no hay interbloqueo causado por la asignación del búfer de emergencia.


2
2017-10-16 06:21



En realidad, se especifica que si la asignación para el objeto de excepción falla, bad_alloc debe lanzarse y la implementación también podría llamar al nuevo controlador.

Esto es lo que se especifica actualmente en la sección estándar de C ++ (§3.7.4.1). [basic.stc.dynamic.allocation]:

Una función de asignación que no puede asignar almacenamiento puede invocar a la función de controlador nuevo instalada actualmente   (21.6.3.3), si hay alguno. [   Nota:   Una función de asignación suministrada por el programa puede obtener la dirección de la actual   instalado   new_handler   utilizando el   std :: get_new_handler   función (21.6.3.4).   - nota final   ] Si una asignación   función que tiene una especificación de excepción que no arroja (18.4) no puede asignar almacenamiento, devolverá un valor nulo   puntero. Cualquier otra función de asignación que no pueda asignar almacenamiento deberá indicar la falla solo lanzando un   excepción (18.1) de un tipo que coincidiría con un controlador (18.3) de tipo   std :: bad_alloc   (21.6.3.1).

Entonces esto recordó en [excepto.terminado]

En algunas situaciones, se debe abandonar el manejo de excepciones para técnicas de manejo de errores menos sutiles. [   Nota:   Estas situaciones son:   -   (1.1)   cuando el mecanismo de manejo de excepciones, después de completar la inicialización del objeto de excepción pero   antes de la activación de un controlador para la excepción (18.1) *

Entonces, el itanium ABI no sigue la especificación estándar de c ++, ya que puede bloquear o llamar terminate si el programa no puede asignar memoria para el objeto de excepción.


0
2017-10-15 10:38