¿Lanzar una excepción mientras se construye un objeto durante un lanzamiento?

Tengo una clase de excepción:

class MyException : public std::exception
{
public:
    MyException( char* message )
        : message_( message )
    {
        if( !message_ ) throw std::invalid_argument("message param must not be null");
    }
};

Y en mi sitio de tiro:

try {
    throw MyException( NULL );
}
catch( std::exception const& e ) {
    std::cout << e.what();
}

(el código no fue compilado, así que disculpe cualquier error)

Me pregunto qué sucederá cuando arroje desde un constructor mientras construyo debido a otro lanzamiento. Supongo que esto es legal, y la captura terminará atrapando a un std::invalid_argument, y la excepción anterior lanzada (MyException) será ignorado o cancelado.

Mi objetivo con este diseño es aplicar invariantes en mi clase de excepción. message_ nunca debe ser NULL, y no quiero tener condiciones para verificar si es nulo en mi what() sobrecarga, así que los verifico en el constructor y tiro si no son válidos.

¿Es esto correcto, o el comportamiento es diferente?

preguntado el 03 de mayo de 12 a las 19:05

Siempre y cuando lo atrapes en alguna parte... ¿has considerado usar const std::string& message entonces esto no es un problema? -

Si deriva de std::runtime_error, no necesita almacenar el mensaje usted mismo. Pase el mensaje al constructor de std::runtime_error. Nota: Algunas implementaciones populares pero erróneas le permiten pasar el mensaje a std::exception, lamentablemente esto no cumple con los estándares y causará problemas al realizar la migración. -

@LokiAstari no uso std::runtime_error (aunque me gustaría) porque requiere la completa what() cuerda que se formará durante la construcción. Me gusta posponer ese procesamiento de cadenas hasta what() se llama, así que uso std::exception para este propósito. -

@RobertDailey: No entiendo de qué estás hablando. std::exception no tiene soporte para la evaluación perezosa de qué cadena. PD. También creo que es una optimización falsa. -

@LokiAstari Estás malinterpretando. Simplemente pospongo la ejecución de mi lógica de construcción de cadenas hasta que se llama what(). std::runtime_error almacena una cadena internamente y requiere uno en su constructor. Esto prácticamente te obliga a construir la cadena para what() cuando construyes tu objeto de excepción. ¿Tener sentido? -

3 Respuestas

El objeto que pretendes lanzar (en este caso MyException) debe ser construido con éxito antes se puede tirar. Así que aún no lo vas a tirar, ya que no se ha construido.

Entonces esto funcionará, lanzando la excepción desde dentro MyExceptionconstructor de . No activará el "lanzamiento de una excepción mientras se maneja una causa de excepción std::terminate" asunto.

contestado el 03 de mayo de 12 a las 19:05

Estaba tratando de encontrar esto en el estándar, pero no vi nada. ¿Se menciona este caso específico en la norma de 2003? - puntero.void

@RobertDailey: No; ¿Por qué sería? Antes throw puede ejecutar, primero debe evaluar con éxito su expresión. y hasta throw se ejecuta, todavía no se ha lanzado nada. Esa expresión no se evaluó con éxito, por lo que throw nunca ocurrió. Es la consecuencia inevitable de muchas partes separadas de C++. - Nicol bolas

@NicolBolas: Tu suposición es correcta, pero no compro el argumento. Se podría argumentar (no es que yo lo sea) que el lanzamiento ha sido iniciado tan pronto como cualquier parte del throw statement se ha iniciado (la instrucción throw incluye la expresión throw), es decir, después de cruzar la sequence point de la última declaración. Afortunadamente, el estándar es explícito sobre el tema, consulte: 15.1.7 (en n3376) - martín york

15.1 Lanzar una excepción n3376

7 el párrafo

Si el mecanismo de manejo de excepciones, después de completar la evaluación de la expresión que se va a lanzar pero antes de que se detecte la excepción, llama a una función que sale a través de una excepción, se llama a std :: terminate (15.5.1).

Esto significa que hasta que el constructor (del objeto que se lanza en este caso) se complete, no sucederá nada especial. Pero después de que el constructor complete, cualquier otra excepción no detectada dará como resultado terminate() siendo llamado.

El estándar continúa proporcionando un ejemplo:

struct C
{
       C() { }
       C(const C&) { throw 0; }
};

int main()
{
  try
  {
    throw C();   // calls std::terminate()
  }
  catch(C) { }
}

Aquí se llama a terminar porque el objeto se crea primero. Pero luego se llama a la construcción de copia para copiar la excepción en la ubicación de espera (15.1.4). Durante esta llamada de función (construcción de copia) se genera una excepción no detectada y, por lo tanto, se llama a terminar.

Entonces, su código como se muestra debería funcionar como se esperaba.

  • O bien: un MyException se genera con un buen mensaje y se lanza
  • O: un std::invalid_argument se genera y se arroja

Respondido el 20 de junio de 20 a las 10:06

Si desea verificar invariantes que siempre deberían ser verdaderos, debe usar aserciones. Las excepciones están destinadas a situaciones inusuales que se espera que sucedan, como casos extremos o una mala entrada del usuario.

Si usa excepciones para informar errores en su código, podría ocultarlos accidentalmente con catch (...). Esto es especialmente importante si está escribiendo código de biblioteca, porque nunca sabe si otras personas podrían detectar e ignorar la excepción, incluso si eso significa que el sistema ha llegado a un estado no válido.

Otra ventaja de las aserciones es que puede deshabilitarlas por completo si lo desea, de modo que ya no incurrirán en ninguna penalización de rendimiento. Esto le permite ser realmente paranoico con esos controles en sus compilaciones de depuración y aún así tener una compilación de lanzamiento ultrarrápida.

contestado el 03 de mayo de 12 a las 19:05

La desventaja de las aserciones es que generalmente siempre están deshabilitadas en tiempo de ejecución. Hay momentos en los que, aunque es algo que siempre debería ser cierto, aún desea verificarlo en tiempo de ejecución, incluso en el modo de lanzamiento. - David Stone

Si un programador quiere ocultar todas las excepciones con catch(...), entonces eso es solo una mala práctica de programación. También podrías argumentar en contra assert porque eso generalmente se implementa como una llamada para salir (o alguna variante), y podría registrar una función para ser llamada en la salida que simplemente vuelve a la ejecución normal y oculta los errores de esa manera. No puede defenderse de un programador que tiene acceso al código y está trabajando en su contra. - David Stone

No tiene que usar la macro estándar assert(), también puede definir la suya propia. De esa manera, puede tener aserciones que deberían deshabilitarse en las versiones de lanzamiento y otras que deberían permanecer. Si lanza una excepción, debe ser una parte documentada de la interfaz y quien llame a su función o constructor que pueda lanzar la excepción tiene que pensar en cómo manejarla. El caso que describe el OP no se puede manejar de ninguna manera razonable que no sea decirle al usuario que el programa tiene un error y se bloqueó. Si no hay una forma razonable de manejarlo, no debería ser una excepción. - benjamin schug

Siempre hay una manera razonable de manejar las cosas. Tengo un programa que busca en un árbol de juego a cierta profundidad. A veces, entro en un estado inválido del que parece no haber recuperación. Sin embargo, hago esa búsqueda en una copia de mis datos reales (la búsqueda, naturalmente, implica muchas copias). Si entro en ese estado, podría usar aserciones (¡el estado siempre debe ser válido!), o podría lanzar una excepción. Elegí lanzar una excepción, porque mi forma de manejarlo es simplemente informar el error y devolver los resultados de la búsqueda en profundidad - 1. Definí profundidad = 0 como "resultado aleatorio". - David Stone

Ese mismo programa inicia sesión en un servidor para jugar. Podría usar una afirmación "siempre dentro" que termina el programa en algo como "el socket se cerró inesperadamente" o "el servidor envió datos no válidos". Eso último significa que no puedo confiar en que mi conexión sea válida y no tengo forma de saber dónde comienza un mensaje y termina otro. Uso excepciones porque mi comportamiento es cerrar el socket, esperar 10 segundos y volver a conectarme al servidor. No manejar una excepción es lo mismo que llamar a terminar, pero le da a los usuarios la opción de manejarla. No quiero decidir que un error siempre debe ser fatal. - David Stone

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.