¿Por qué las especificaciones de excepción son malas?

50

En la escuela, hace más de 10 años, te estaban enseñando a usar especificadores de excepción. Dado que mi experiencia es como uno de ellos programadores de Torvaldish C que obstinadamente evita C ++ a menos que sea forzado a hacerlo, solo termino en C ++ esporádicamente, y cuando lo hago, todavía uso especificadores de excepción, ya que eso es lo que me enseñaron.

Sin embargo, la mayoría de los programadores de C ++ parecen desaprobar los especificadores de excepción. He leído el debate y los argumentos de varios gurús de C ++, como estos . Por lo que yo entiendo, se reduce a tres cosas:

  1. Los especificadores de excepción usan un sistema de tipos que es inconsistente con el resto del lenguaje ("sistema de tipo de sombra").
  2. Si su función con un especificador de excepción arroja algo más que lo que ha especificado, el programa se terminará de manera incorrecta e inesperada.
  3. Los especificadores de excepción se eliminarán en el próximo estándar de C ++.

¿Me estoy perdiendo algo aquí o son todas estas razones?

Mis propias opiniones

Con respecto a 1): ¿Y qué? C ++ es probablemente el lenguaje de programación más inconsistente jamás creado, en cuanto a sintaxis. Tenemos las macros, las goto / etiquetas, la horda (¿acaparamiento?) De comportamiento indefinido / no especificado / implementado, los tipos enteros mal definidos, todas las reglas implícitas de promoción de tipos, palabras clave de casos especiales como amigo, auto , registro, explícito ... Y así sucesivamente. Alguien probablemente podría escribir varios libros gruesos de todas las rarezas en C / C ++. Entonces, ¿por qué las personas reaccionan contra esta inconsistencia particular, que es un defecto menor en comparación con muchas otras características mucho más peligrosas del lenguaje?

Con respecto a 2): ¿No es mi responsabilidad? Hay muchas otras formas en que puedo escribir un error fatal en C ++, ¿por qué este caso particular es peor? En lugar de escribir throw(int)y luego lanzar Crash_t, también puedo afirmar que mi función devuelve un puntero a int, luego hacer un tipo de letra explícito y explícito y devolver un puntero a un Crash_t. El espíritu de C / C ++ siempre ha sido dejar la mayor parte de la responsabilidad al programador.

¿Qué pasa con las ventajas entonces? Lo más obvio es que si su función intenta arrojar explícitamente cualquier tipo que no sea el que especificó, el compilador le dará un error. Creo que el estándar es claro con respecto a esto (?). Los errores solo sucederán cuando su función llame a otras funciones que a su vez arrojan el tipo incorrecto.

Al provenir de un mundo de programas deterministas e integrados en C, sin duda preferiría saber exactamente qué me arrojará una función. Si hay algo en el lenguaje que lo respalde, ¿por qué no usarlo? Las alternativas parecen ser:

void func() throw(Egg_t);

y

void func(); // This function throws an Egg_t

Creo que hay una gran posibilidad de que la persona que llama ignore / olvide implementar el try-catch en el segundo caso, y menos en el primer caso.

Según tengo entendido, si cualquiera de estas dos formas decide lanzar repentinamente otro tipo de excepción, el programa se bloqueará. En el primer caso porque no se le permite lanzar otra excepción, en el segundo caso porque nadie esperaba que arrojara un SpanishInquisition_t y, por lo tanto, esa expresión no se capta donde debería haber estado.

En el caso de esto último, tener alguna captura de último recurso (...) en el nivel más alto del programa realmente no parece mejor que un bloqueo del programa: "Oye, en algún lugar de tu programa algo arrojó una excepción extraña y no controlada ". No puede recuperar el programa una vez que esté tan lejos de donde se produjo la excepción, lo único que puede hacer es salir del programa.

Y desde el punto de vista del usuario, no podría importarles menos si recibe un cuadro de mensaje malicioso del sistema operativo que dice "Programa terminado. Blablabla en la dirección 0x12345" o un cuadro de mensaje maligno de su programa que dice "Excepción no controlada: myclass. func.something ". El error sigue ahí.


Con el próximo estándar C ++ no tendré otra opción que abandonar los especificadores de excepción. Pero preferiría escuchar algún argumento sólido de por qué son malos, en lugar de "Su Santidad lo ha declarado y así es así". ¿Quizás hay más argumentos en contra de ellos que los que enumeré, o tal vez hay más de ellos de lo que me doy cuenta?


fuente
30
Estoy tentado a desestimar esto como "despotricar, disfrazado de pregunta". Usted pregunta tres puntos válidos sobre E.specs, pero ¿por qué nos molesta con sus divagaciones de C ++, que es tan molesto y me gusta C?
Martin Ba
10
@ Martin: Porque quiero señalar que soy parcial y no estoy al día con todos los detalles, pero también que considero el lenguaje con ojos ingenuos y / o aún no arruinados. Pero también que C y C ++ ya son lenguajes increíblemente defectuosos, por lo que un defecto más o menos realmente no importa. La publicación fue mucho peor antes de editarla :) Los argumentos en contra de los especificadores de excepciones también son bastante subjetivos y, por lo tanto, es difícil discutirlos sin ser subjetivo.
SpanishInquisition_t! ¡Divertidísimo! Personalmente, me impresionó el uso del puntero de marco de pila para lanzar excepciones y parece que puede hacer que el código sea mucho más limpio. Sin embargo, nunca escribí código con excepciones. Llámame anticuado, pero los valores de retorno funcionan bien para mí.
Shahbaz
@Shahbaz Como se puede leer entre líneas, yo también soy bastante anticuado, pero todavía nunca me he preguntado si las excepciones en sí mismas son buenas o malas.
2
El tiempo pasado de tiro es tiró , no throwed . Es un verbo fuerte.
TRiG

Respuestas:

48

Las especificaciones de excepción son malas porque se aplican de manera débil y, por lo tanto, en realidad no logran mucho, y también son malas porque obligan al tiempo de ejecución a verificar excepciones inesperadas para que puedan terminar (), en lugar de invocar UB , esto puede desperdiciar una cantidad significativa de rendimiento.

En resumen, las especificaciones de excepción no se aplican con la suficiente fuerza en el lenguaje como para hacer que el código sea más seguro, e implementarlas según lo especificado fue una gran pérdida de rendimiento.

DeadMG
fuente
2
Pero la verificación en tiempo de ejecución no es culpa de la especificación de excepción como tal, sino de cualquier parte del estándar que establezca que debería haber una terminación si se arroja el tipo incorrecto. ¿No sería más inteligente simplemente eliminar el requisito que lleva a esta verificación dinámica y dejar que todo sea evaluado estáticamente por el compilador? Parece que RTTI es el verdadero culpable aquí, o ...?
77
@Lundin: no es posible determinar estáticamente todas las excepciones que se pueden generar desde una función en C ++, porque el lenguaje permite cambios arbitrarios y no deterministas en el flujo de control en tiempo de ejecución (por ejemplo, mediante punteros de función, métodos virtuales y similares). La implementación de una comprobación de excepciones de tiempo de compilación estática como Java requeriría cambios fundamentales en el lenguaje y mucha compatibilidad con C para deshabilitarse.
3
@OrbWeaver: Creo que las comprobaciones estáticas son bastante posibles: la especificación de excepción sería parte de la especificación de una función (como el valor de retorno), y los punteros de función, anulaciones virtuales, etc. tendrían que tener especificaciones compatibles. La principal objeción sería que es una característica de todo o nada: las funciones con especificaciones de excepción estáticas no pueden llamar funciones heredadas. (Llamar a las funciones de C estaría bien, ya que no pueden lanzar nada).
Mike Seymour
2
@Lundin: las especificaciones de excepción no se han eliminado, solo han quedado en desuso. El código heredado que los usa seguirá siendo válido.
Mike Seymour
1
@Lundin: las versiones anteriores de C ++ con especificaciones de excepción son perfectamente compatibles. No fallarán en compilar ni ejecutarse inesperadamente si el código era válido anteriormente.
DeadMG
18

Una razón por la que nadie los usa es porque su suposición principal es incorrecta:

"La [ventaja] más obvia es que si su función intenta arrojar explícitamente cualquier tipo que no sea el que especificó, el compilador le dará un error".

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Este programa se compila sin errores ni advertencias .

Además, aunque se hubiera detectado la excepción , el programa aún finaliza.


Editar: para responder un punto en su pregunta, catch (...) (o mejor, catch (std :: exeption &) u otra clase base, y luego catch (...)) sigue siendo útil incluso si no lo hace saber exactamente lo que salió mal.

Tenga en cuenta que su usuario ha pulsado un botón de menú para "guardar". El controlador del botón de menú invita a la aplicación a guardar. Esto podría fallar por innumerables razones: el archivo estaba en un recurso de red que desapareció, había un archivo de solo lectura y no se pudo guardar, etc. Pero al controlador realmente no le importa por qué algo falló; solo le importa que haya tenido éxito o haya fallado. Si tuvo éxito, todo está bien. Si falla, puede decirle al usuario. La naturaleza exacta de la excepción es irrelevante. Además, si escribe un código adecuado y seguro para excepciones, significa que cualquier error de este tipo puede propagarse a través de su sistema sin desactivarlo, incluso para el código que no se preocupa por el error. Esto hace que sea más fácil extender el sistema. Por ejemplo, ahora guarda a través de una conexión de base de datos.

Kaz Dragon
fuente
44
@Lundin lo compiló sin advertencias. Y no, no puedes. No confiablemente. Puede llamar a través de punteros de función o podría ser una función miembro virtual, y la clase derivada genera una excepción derivada (que la clase base posiblemente no puede conocer).
Kaz Dragon
Mala suerte Embarcadero / Borland da una advertencia para esto: "lanzar excepción viola especificador de excepción". Aparentemente, GCC no. Sin embargo, no hay razón para que no puedan implementar una advertencia. Y de manera similar, una herramienta de análisis estático podría encontrar fácilmente este error.
14
@Lundin: Por favor, no comience a discutir y repetir información falsa. Su pregunta ya era una queja, y reclamar cosas falsas no ayuda. Una herramienta de análisis estático NO PUEDE encontrar si esto sucede; hacerlo implicaría resolver el problema de detención. Además, necesitaría analizar todo el programa, y ​​muy a menudo toda la fuente no está disponible.
David Thornley
1
@Lundin, la misma herramienta de análisis estático podría decirle qué excepciones lanzadas abandonan su pila, por lo que, en este caso, el uso de especificaciones de excepción aún no le compra nada excepto un posible falso negativo que derribará su programa donde podría haber manejado el caso de error en una forma mucho más agradable (como anunciar el fallo y seguir corriendo: vea la segunda mitad de mi respuesta para ver un ejemplo).
Kaz Dragon
1
@Lundin Una buena pregunta. La respuesta es que, en general, no sabe si las funciones que está llamando generarán excepciones, por lo que la suposición segura es que todas lo hacen. Dónde encontrarlos es una pregunta que respondí en stackoverflow.com/questions/4025667/… . Los controladores de eventos en particular forman límites de módulos naturales. Permitir que las excepciones escapen a un sistema genérico de ventana / evento (por ejemplo) será castigado. El controlador debería atraparlos a todos si el programa va a sobrevivir allí.
Kaz Dragon
17

Esta entrevista con Anders Hejlsberg es bastante famosa. En él, explica por qué el equipo de diseño de C # descartó las excepciones verificadas en primer lugar. En pocas palabras, hay dos razones principales: versionabilidad y escalabilidad.

Sé que el OP se está centrando en C ++, mientras que Hejlsberg está discutiendo C #, pero los puntos que Hejlsberg hace son perfectamente aplicables a C ++ también.

CesarGon
fuente
55
"los puntos que señala Hejlsberg son perfectamente aplicables a C ++ también" .. ¡Incorrecto! Las excepciones marcadas implementadas en Java obligan a la persona que llama a tratar con ellas (y / o la persona que llama, etc.). Las especificaciones de excepción en C ++ (aunque son bastante débiles e inútiles) solo se aplican a un único "límite de llamada de función" y no se "propagan por la pila de llamadas" como hacen las excepciones marcadas.
Martin Ba
3
@ Martin: Estoy de acuerdo con usted sobre el comportamiento de las especificaciones de excepción en C ++. Pero mi frase que cita "los puntos que Hejlsberg hace son perfectamente aplicables a C ++ también" se refiere, bueno, a los puntos que hace Hejlsberg, en lugar de las especificaciones de C ++. En otras palabras, estoy hablando aquí sobre la capacidad de versión y escalabilidad del programa, en lugar de la propagación de excepciones.
CesarGon
3
No estuve de acuerdo con Hejlsberg desde el principio: "La preocupación que tengo sobre las excepciones comprobadas son las esposas que ponen en los programadores. Ves a los programadores recogiendo nuevas API que tienen todas estas cláusulas de lanzamiento, y luego ves cuán complicado es su código, y te das cuenta las excepciones marcadas no los están ayudando. Es una especie de diseñadores dictatoriales de API que le dicen cómo manejar sus excepciones ". --- Las excepciones se verifican o no, aún debe manejar las excepciones lanzadas por la API a la que llama. Las excepciones marcadas hacen que simplemente sea explícito.
quant_dev
55
@quant_dev: No, no tiene que manejar las excepciones lanzadas por la API que está llamando. Una opción perfectamente válida es no manejar las excepciones y dejar que aumenten la pila de llamadas para que su interlocutor las maneje.
CesarGon
44
@CesarGon No manejar excepciones también es elegir cómo manejarlas.
quant_dev