es 'catch (...) {throw; } 'una mala práctica?

74

Aunque estoy de acuerdo en que capturar ... sin volver a generar es realmente incorrecto, sin embargo, creo que usar construcciones como esta:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

Es aceptable en los casos donde RAII no es aplicable . (Por favor, no preguntes ... no a todos en mi empresa les gusta la programación orientada a objetos y RAII a menudo se ve como "cosas escolares inútiles" ...)

Mis compañeros de trabajo dicen que siempre debes saber qué excepciones se deben lanzar y que siempre puedes usar construcciones como:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

¿Existe una buena práctica bien recibida con respecto a estas situaciones?

    
pregunta ereOn 05.12.2011 - 10:05

6 respuestas

189
  

Mis compañeros de trabajo dicen que siempre debes saber qué excepciones se deben lanzar [...]

Su compañero de trabajo, odiaría decirlo, obviamente nunca ha trabajado en bibliotecas de uso general.

¿Cómo en el mundo puede una clase como std::vector incluso fingir saber qué lanzarán los constructores de copias, a la vez que garantiza una seguridad excepcional?

Si siempre supieras lo que haría la persona llamada en tiempo de compilación, ¡el polimorfismo sería inútil! A veces, el objetivo entero es abstraer lo que sucede en un nivel inferior, por lo que específicamente no quiere saber qué está pasando.

    
respondido por el Mehrdad 05.12.2011 - 10:07
43

Lo que parece que estás atrapado es el infierno específico de alguien que intenta tener su pastel y comérselo también.

RAII y las excepciones están diseñadas para ir de la mano. RAII es el medio por el cual usted no tiene tener para escribir muchas declaraciones catch(...) para realizar la limpieza. Ocurrirá automáticamente, como cuestión de rutina. Y las excepciones son la única forma de trabajar con objetos RAII, porque los constructores solo pueden tener éxito o lanzar (o poner el objeto en un estado de error, pero ¿quién quiere eso?).

Una declaración catch puede hacer una de dos cosas: manejar un error o circunstancia excepcional, o hacer un trabajo de limpieza. A veces hace ambas cosas, pero cada declaración catch existe para hacer al menos una de estas.

catch(...) es incapaz de realizar el manejo adecuado de excepciones. No sabes cuál es la excepción; No puede obtener información sobre la excepción. No tiene absolutamente ninguna información más que el hecho de que algo haya lanzado una excepción dentro de un bloque de código determinado. Lo único legítimo que puedes hacer en un bloque así es hacer la limpieza. Y eso significa volver a lanzar la excepción al final de la limpieza.

Lo que le da RAII con respecto al manejo de excepciones es una limpieza gratuita. Si todo está encapsulado en RAII correctamente, entonces todo se limpiará correctamente. Ya no es necesario tener catch de instrucciones para realizar la limpieza. En cuyo caso, no hay razón para escribir una declaración catch(...) .

Entonces, estoy de acuerdo en que catch(...) es mayormente malo ... provisionalmente .

Esa disposición es el uso adecuado de RAII. Porque sin ella, necesitas para poder realizar cierta limpieza. No hay manera de evitarlo; Tienes que ser capaz de hacer trabajos de limpieza. Debe poder asegurarse de que lanzar una excepción deje el código en un estado razonable. Y catch(...) es una herramienta vital al hacerlo.

No puedes tener uno sin el otro. No puedes decir que tanto RAII como catch(...) son malos. Necesitas al menos uno de estos; de lo contrario, no es una excepción segura.

Por supuesto, hay un uso válido aunque raro de catch(...) que ni siquiera RAII puede eliminar: obtener un exception_ptr para reenviarlo a otra persona. Por lo general, a través de una interfaz promise/future o similar.

  

Mis compañeros de trabajo dicen que siempre debes saber qué excepciones se deben lanzar y que siempre puedes usar construcciones como:

Tu compañero de trabajo es un idiota (o simplemente terriblemente ignorante). Esto debería ser inmediatamente obvio debido a la cantidad de código de copiar y pegar que sugiere que escribas. La limpieza para cada una de esas declaraciones de captura será exactamente igual . Eso es una pesadilla de mantenimiento, sin mencionar la legibilidad.

En resumen: este es el problema que RAII se creó para resolver (no es que no resuelva otros problemas).

Lo que me confunde de esta noción es que generalmente se trata de cómo la mayoría de las personas argumentan que el RAII es malo. En general, el argumento es que "RAII es malo porque tienes que usar excepciones para señalar el fallo del constructor. Pero no puedes lanzar excepciones, porque no es seguro y tendrás que tener muchas declaraciones catch para limpiar todo". " Que es un argumento roto porque RAII resuelve el problema que crea la falta de RAII.

Lo más probable es que esté en contra de RAII porque oculta los detalles. Las llamadas a los destructores no son visibles inmediatamente en las variables automáticas. Entonces obtienes código que es llamado implícitamente. Algunos programadores realmente odian eso. Aparentemente, hasta el punto en el que piensan que tener un 3% de declaraciones de catch , todas las cuales hacen lo mismo con el código de copiar y pegar es una mejor idea.

    
respondido por el Nicol Bolas 05.12.2011 - 12:00
14

Dos comentarios, de verdad. La primera es que mientras estás en un mundo ideal, Siempre se debe saber qué excepciones se pueden lanzar, en la práctica, si estás tratando con bibliotecas de terceros, o compilando con un Microsoft compilador, no lo haces. Más al punto, sin embargo; incluso si lo sabes exactamente todas las posibles excepciones, ¿es relevante aquí? catch (...) expresa la intención mucho mejor que catch ( std::exception const& ) , incluso suponiendo que todas las excepciones posibles se deriven de std::exception (lo que sería el caso en un mundo ideal). Como para utilizando varios bloques de captura, si no hay una base común para todos excepciones: esto es una ofuscación absoluta, y una pesadilla de mantenimiento. ¿Cómo reconoces que todos los comportamientos son idénticos? Y eso Esa fue la intención? ¿Y qué pasa si tienes que cambiar el comportamiento (corrección de errores, por ejemplo)? Es muy fácil perder uno.

    
respondido por el James Kanze 05.12.2011 - 10:21
11

Creo que su compañero de trabajo ha confundido algunos buenos consejos: solo debe manejar las excepciones conocidas en un bloque catch cuando no las vuelva a lanzar.

Esto significa:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

Es malo porque ocultará silenciosamente cualquier error

.

Sin embargo:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

Está bien: sabemos con qué estamos lidiando y no es necesario que lo expongamos al código de llamada.

Igualmente:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

Está bien, incluso las mejores prácticas, el código para tratar los errores generales debe estar con el código que los causa. Es mejor que confiar en la persona que recibe la llamada para saber que una transacción debe retrotraerse o lo que sea.

    
respondido por el Keith 05.12.2011 - 11:27
9

Cualquier respuesta de sí o no debe ir acompañada de una explicación de por qué esto es así.

Decir que está mal simplemente porque me enseñaron de esa manera es solo fanatismo ciego.

Escribir el mismo //Some cleanup; throw varias veces, como en tu ejemplo es incorrecto porque es una duplicación de código y eso es una carga de mantenimiento. Escribirlo solo una vez es mejor.

Escribir un catch(...) para silenciar todas las excepciones es incorrecto porque solo debes manejar las excepciones que sabes cómo manejar, y con ese comodín puedes hacer más de lo que esperas, y hacerlo puede silenciar errores importantes.

Pero si vuelves a lanzar después de catch(...) , la última razón ya no se aplica, ya que en realidad no estás manejando la excepción, por lo que no hay razón para desalentar esto.

En realidad, he hecho esto para iniciar sesión en funciones confidenciales sin ningún problema en absoluto:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}
    
respondido por el rodrigo 05.12.2011 - 10:32
2

En general, estoy de acuerdo con el estado de ánimo de las publicaciones aquí, realmente me disgusta el patrón de captura de excepciones específicas. Creo que la sintaxis de esto todavía está en su infancia y todavía no es capaz de hacer frente al código redundante.

Pero como todos están diciendo eso, hablaré con el hecho de que, aunque los uso con moderación, a menudo he mirado una de mis declaraciones de "captura (Excepción e)" y dije "Maldita sea, me gustaría Había llamado a las excepciones específicas en ese momento "porque cuando llegas más tarde, a menudo es bueno saber cuál fue la intención y qué es lo que el cliente puede echar un vistazo.

No estoy justificando la actitud de "Usar siempre x", solo digo que de vez en cuando es bueno verlos en la lista y estoy seguro de que por eso algunas personas piensan que es la forma "correcta" de ir.

    
respondido por el Bill K 05.12.2011 - 19:05

Lea otras preguntas en las etiquetas