¿Cómo puedo crear y aplicar contratos para excepciones?

33

Estoy tratando de convencer al líder de mi equipo para que permita usar excepciones en C ++ en lugar de devolver un bool isSuccessful o una enumeración con el código de error. Sin embargo, no puedo contrarrestar esta crítica suya.

Considera esta biblioteca:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Considere un desarrollador que llama a la B()función. Comprueba su documentación y ve que no devuelve excepciones, por lo que no trata de atrapar nada. Este código podría bloquear el programa en producción.

  2. Considere un desarrollador que llama a la C()función. No verifica la documentación, por lo que no detecta ninguna excepción. La llamada no es segura y podría bloquear el programa en producción.

Pero si buscamos errores de esta manera:

void old_C(myenum &return_code);

El compilador advertirá a un desarrollador que use esa función si no proporciona ese argumento, y él diría "Ajá, esto devuelve un código de error que debo verificar".

¿Cómo puedo usar las excepciones de forma segura para que haya algún tipo de contrato?

DBedrenko
fuente
44
@Sjoerd La principal ventaja es que el compilador obliga a un desarrollador a proporcionar una variable a la función para almacenar el código de retorno; se le hace saber que puede haber un error y debe manejarlo. Eso es infinitamente mejor que las excepciones, que no tienen verificación de tiempo de compilación.
DBedrenko
44
"él debería manejarlo": tampoco se verifica si esto sucede.
Sjoerd
44
@Sjoerd No es necesario. Es lo suficientemente bueno como para que el desarrollador sepa que puede ocurrir un error y que debe verificarlo. También está a la vista de los revisores, tanto si verificaron un error como si no.
DBedrenko
55
Es posible que tenga una mejor oportunidad de usar un Either/ Resultmónada para devolver el error de una manera
composable
55
@gardenhead Debido a que muchos desarrolladores no lo usan correctamente, terminan con un código feo y proceden a culpar a la característica en lugar de a ellos mismos. La característica de excepción marcada en Java es algo hermoso, pero tiene una mala reputación porque algunas personas simplemente no la entienden.
AxiomaticNexus

Respuestas:

47

Esta es una crítica legítima de las excepciones. A menudo son menos visibles que el simple manejo de errores, como devolver un código. Y no hay una manera fácil de hacer cumplir un "contrato". Parte del punto es permitirle permitir que las excepciones se detecten en un nivel superior (si tiene que detectar cada excepción en cada nivel, ¿qué tan diferente es devolver un código de error, de todos modos?). Y esto significa que su código podría ser llamado por algún otro código que no lo maneje adecuadamente.

Las excepciones tienen inconvenientes; Tiene que presentar un caso basado en el costo-beneficio.
Estos dos artículos me parecieron útiles: la necesidad de excepciones y todo lo que está mal con las excepciones. Además, esta publicación de blog ofrece opiniones de muchos expertos sobre excepciones, con un enfoque en C ++ . Si bien la opinión de expertos parece inclinarse a favor de las excepciones, está lejos de ser un consenso claro.

En cuanto a convencer al líder de su equipo, esta podría no ser la batalla adecuada para elegir. Especialmente no con código heredado. Como se señaló en el segundo enlace anterior:

Las excepciones no se pueden propagar a través de ningún código que no sea seguro para excepciones. Por lo tanto, el uso de excepciones implica que todo el código en el proyecto debe ser seguro para excepciones.

Agregar un poco de código que usa excepciones a un proyecto que principalmente no lo hace probablemente no será una mejora. No usar excepciones en un código bien escrito está lejos de ser un problema catastrófico; puede no ser un problema en absoluto, dependiendo de la aplicación y del experto que pregunte. Tienes que elegir tus batallas.

Probablemente este no sea un argumento en el que gaste esfuerzo, al menos hasta que se inicie un nuevo proyecto. E incluso si tiene un nuevo proyecto, ¿lo usará o lo usará algún código heredado?


fuente
1
Gracias por el consejo y los enlaces. Este es un proyecto nuevo, no uno heredado. Me pregunto por qué las excepciones están tan extendidas cuando tienen un inconveniente tan grande que hace que su uso sea inseguro. Si detecta una excepción de n niveles más altos, luego modifica el código y lo mueve, no hay verificación estática para garantizar que la excepción aún se detecte y se maneje (y es demasiado pedirle a los revisores que verifiquen todas las funciones de n niveles de profundidad )
DBedrenko
3
@Dee vea los enlaces anteriores para ver algunos argumentos a favor de las excepciones (he agregado un enlace adicional con una opinión más experta).
66
¡Esta respuesta es acertada!
Doc Brown
1
Puedo afirmar por experiencia que tratar de agregar excepciones a una base de código preexistente es REALMENTE una mala idea. He trabajado con una base de código así y fue REALMENTE, REALMENTE difícil trabajar porque nunca sabías lo que iba a estallar en tu cara. Las excepciones SOLO se deben usar en proyectos donde los planifique por adelantado.
Michael Kohne
26

Literalmente, hay libros enteros escritos sobre este tema, por lo que cualquier respuesta será, en el mejor de los casos, un resumen. Estos son algunos de los puntos importantes que creo que valen la pena, según su pregunta. No es una lista exhaustiva.


Las excepciones están destinadas a NO ser atrapadas por todo el lugar.

Siempre que haya un controlador de excepción general en el bucle principal, dependiendo del tipo de aplicación (servidor web, servicio local, utilidad de línea de comando ...), generalmente tiene todos los controladores de excepción que necesita.

En mi código, solo hay unas pocas declaraciones catch, si es que hay alguna, fuera del ciclo principal. Y ese parece ser el enfoque común en C ++ moderno.


Las excepciones y los códigos de retorno no son mutuamente excluyentes.

No debe hacer de este un enfoque de todo o nada. Las excepciones deben usarse para situaciones excepcionales. Cosas como "Archivo de configuración no encontrado", "Disco lleno" o cualquier otra cosa que no se pueda manejar localmente.

Las fallas comunes, como verificar si un nombre de archivo proporcionado por el usuario es válido, no es un caso de uso para excepciones; Utilice un valor de retorno en esos casos en su lugar.

Como puede ver en los ejemplos anteriores, "archivo no encontrado" puede ser una excepción o un código de retorno, dependiendo del caso de uso: "es parte de la instalación" versus "el usuario puede hacer un error tipográfico".

Entonces no hay una regla absoluta. Una pauta aproximada es: si se puede manejar localmente, conviértalo en un valor de retorno; Si no puede manejarlo localmente, inicie una excepción.


La comprobación estática de excepciones no es útil.

Como las excepciones no se deben manejar localmente de todos modos, generalmente no es importante qué excepciones se pueden lanzar. La única información útil es si se puede lanzar alguna excepción.

Java tiene una comprobación estática, pero generalmente se considera un experimento fallido, y la mayoría de los lenguajes ya que, especialmente C #, no tienen ese tipo de comprobación estática. Esta es una buena lectura sobre las razones por las cuales C # no lo tiene.

Por esas razones, C ++ ha dejado de estar throw(exceptionA, exceptionB)a favor de noexcept(true). El valor predeterminado es que una función puede lanzar, por lo que los programadores deben esperar eso a menos que la documentación explícitamente prometa lo contrario.


Escribir código seguro de excepciones no tiene nada que ver con escribir controladores de excepciones.

¡Prefiero decir que escribir código seguro de excepción se trata de cómo evitar escribir manejadores de excepción!

La mayoría de las mejores prácticas tienen la intención de reducir el número de manejadores de excepciones. Escribir código una vez e invocarlo automáticamente, por ejemplo, a través de RAII, da como resultado menos errores que copiar y pegar el mismo código en todo el lugar.

Sjoerd
fuente
3
" Siempre que haya un controlador de excepciones general ... generalmente ya está bien " . Esto contradice la mayoría de las fuentes, lo que subraya que es muy difícil escribir código seguro de excepción.
12
@ dan1111 "Escribir código seguro de excepción" tiene poco que ver con escribir manejadores de excepciones. Escribir código seguro de excepción se trata de usar RAII para encapsular recursos, usar "hacer todo el trabajo localmente primero y luego usar swap / move" y otras mejores prácticas. Esos son desafiantes cuando no estás acostumbrado a ellos. Pero "escribir manejadores de excepciones" no es parte de esas mejores prácticas.
Sjoerd
44
@AxiomaticNexus En algún nivel, puede explicar cada uso fallido de una función como "desarrolladores malos". Las mejores ideas son las que reducen el mal uso porque hacen que hacer lo correcto sea simple. Las excepciones marcadas estáticamente no entran en esa categoría. Crean un trabajo desproporcionado a su valor, a menudo en lugares donde el código que se escribe simplemente no necesita preocuparse por ellos. Los usuarios se sienten tentados a silenciarlos de manera incorrecta para que el compilador deje de molestarlos. Incluso bien hecho, conducen a un montón de desorden.
jpmc26
44
@AxiomaticNexus La razón por la que es desproporcionada es que esto puede propagarse fácilmente a casi todas las funciones en toda su base de código . Cuando todas las funciones de la mitad de mis clases acceden a la base de datos, no todas necesitan declarar explícitamente que puede generar una excepción SQLException; tampoco lo hace cada método de controlador que los llama. Tanto ruido es en realidad un valor negativo, independientemente de cuán pequeña sea la cantidad de trabajo. Sjoerd da en el clavo: la mayor parte de su código no tiene que preocuparse por las excepciones porque de todos modos no puede hacer nada al respecto .
jpmc26
3
@AxiomaticNexus Sí, debido a que los mejores desarrolladores son tan perfectos que su código siempre manejará perfectamente cada entrada posible del usuario, y nunca, nunca, verá esas excepciones en productos. Y si lo haces, significa que eres un fracaso en la vida. En serio, no me des esta basura. Nadie es tan perfecto, ni siquiera el mejor. Y su aplicación debe comportarse de manera sensata incluso ante esos errores, incluso si "sensatamente" es "detener y devolver una página / mensaje de error medio decente". Y adivina qué: eso es lo mismo que haces con el 99% de IOExceptions también . La división marcada es arbitraria e inútil.
jpmc26
8

Los programadores de C ++ no buscan especificaciones de excepción. Buscan garantías de excepción.

Supongamos que un código arroja una excepción. ¿Qué suposiciones puede hacer el programador que seguirán siendo válidas? Por la forma en que está escrito el código, ¿qué garantiza el código después de una excepción?

¿O es posible que una determinada pieza de código pueda garantizar que nunca se lance (es decir, nada menos que el proceso del sistema operativo se termine)?

La palabra "retroceder" aparece con frecuencia en las discusiones sobre excepciones. Poder volver a un estado válido (que está documentado explícitamente) es un ejemplo de garantía de excepción. Si no hay una garantía de excepción, un programa debería finalizar en el acto porque ni siquiera se garantiza que cualquier código que ejecute a partir de entonces funcione según lo previsto; por ejemplo, si la memoria se ha dañado, cualquier otra operación es un comportamiento técnicamente indefinido.

Varias técnicas de programación en C ++ promueven garantías de excepción. RAII (gestión de recursos basada en el alcance) proporciona un mecanismo para ejecutar código de limpieza y garantizar que los recursos se liberen tanto en casos normales como en casos excepcionales. Hacer una copia de los datos antes de realizar modificaciones en los objetos le permite a uno restaurar el estado de ese objeto si la operación falla. Y así.

Las respuestas a esta pregunta de StackOverflow dan una idea de los grandes alcances que los programadores de C ++ van a comprender todos los modos de falla posibles que podrían sucederle a su código e intentan proteger la validez del estado del programa a pesar de las fallas. El análisis línea por línea del código C ++ se convierte en un hábito.

Cuando se desarrolla en C ++ (para uso de producción), uno no puede darse el lujo de pasar por alto los detalles. Además, el blob binario (no de código abierto) es la ruina de los programadores de C ++. Si tengo que llamar a un blob binario, y el blob falla, entonces la ingeniería inversa es lo que haría un programador de C ++ a continuación.

Referencia: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety ; consulte la sección Seguridad de excepciones.

C ++ tuvo un intento fallido de implementar especificaciones de excepción. Un análisis posterior en otros idiomas dice que las especificaciones de excepción simplemente no son prácticas.

Por qué es un intento fallido: para aplicarlo estrictamente, debe ser parte del sistema de tipos. Pero no lo es. El compilador no verifica la especificación de excepción.

Por qué C ++ eligió eso y por qué las experiencias de otros lenguajes (Java) prueban que la especificación de excepción es discutible: a medida que uno modifica la implementación de una función (por ejemplo, necesita hacer una llamada a una función diferente que puede generar un nuevo tipo de excepción), una aplicación estricta de la especificación de excepción significa que también debe actualizar esa especificación. Esto se propaga: puede terminar teniendo que actualizar las especificaciones de excepción para docenas o cientos de funciones para lo que es un cambio simple. Las cosas empeoran para las clases base abstractas (el equivalente en C ++ de las interfaces). Si la especificación de excepción se aplica en las interfaces, las implementaciones de interfaces no podrán invocar funciones que arrojen diferentes tipos de excepciones.

Referencia: http://www.gotw.ca/publications/mill22.htm

Comenzando con C ++ 17, el [[nodiscard]]atributo se puede usar en valores de retorno de función (consulte: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -no puede ser ignorado ).

Entonces, si hago un cambio de código y eso introduce un nuevo tipo de condición de falla (es decir, un nuevo tipo de excepción), ¿es un cambio radical? ¿Debería haber obligado a la persona que llama a actualizar el código, o al menos ser advertido sobre el cambio?

Si acepta los argumentos de que los programadores de C ++ buscan garantías de excepción en lugar de especificaciones de excepción, entonces la respuesta es que si el nuevo tipo de condición de falla no rompe ninguna de las garantías de excepción que el código promete anteriormente, no es un cambio importante.

rwong
fuente
"Cuando uno modifica la implementación de una función (por ejemplo, necesita hacer una llamada a una función diferente que puede generar un nuevo tipo de excepción), una aplicación estricta de la especificación de excepción significa que también debe actualizar esa especificación" - Bien , como deberías. ¿Cuál sería la alternativa? ¿Ignorando la excepción recién introducida?
AxiomaticNexus
@AxiomaticNexus Eso también se responde con una garantía de excepción. De hecho, sostengo que la especificación nunca puede ser completa; La garantía es lo que buscamos. A menudo, comparamos el tipo de excepción en un intento de averiguar qué garantía se rompió, para adivinar qué garantía aún era válida. La especificación de excepción enumera algunos de los tipos que necesita verificar, pero recuerde que en cualquier sistema práctico, la lista no puede estar completa. La mayoría de las veces, obliga a las personas a tomar el atajo de "comer" el tipo de excepción, envolviéndolo en otra excepción, lo que dificulta la verificación del tipo de excepción
rwong
@AxiomaticNexus No estoy argumentando a favor o en contra del uso de la excepción. El uso típico de excepciones fomenta tener una clase de excepción base y algunas clases de excepción derivadas. Mientras tanto, uno debe recordar que otros tipos de excepciones son posibles, por ejemplo, los arrojados desde C ++ STL u otras bibliotecas. Se podría argumentar que la especificación de excepción podría funcionar en este caso: especifique que se std::exceptiongenerarán las excepciones estándar ( ) y su propia excepción base. Pero cuando se aplica a un proyecto completo, simplemente significa que cada función tendrá esta misma especificación: ruido.
rwong
Señalaría que cuando se trata de excepciones, C ++ y Java / C # son lenguajes diferentes. En primer lugar, C ++ no es de tipo seguro en el sentido de permitir la ejecución de código arbitrario o la sobrescritura de datos, en segundo lugar, C ++ no imprime un buen seguimiento de la pila. Estas diferencias obligaron a los programadores de C ++ a tomar diferentes decisiones en términos de errores y manejo de excepciones. También significa que ciertos proyectos podrían afirmar legítimamente que C ++ no es un lenguaje adecuado para ellos. (Por ejemplo, personalmente considero que no se permitirá que la decodificación de imágenes TIFF se implemente en C o C ++.)
rwong
1
@rwong: Un gran problema con el manejo de excepciones en lenguajes que siguen el modelo C ++ es que catchse basa en el tipo del objeto de excepción, cuando en muchos casos lo que importa no es tanto la causa directa de la excepción sino más bien lo que implica sobre El estado del sistema. Desafortunadamente, el tipo de excepción generalmente no dice nada acerca de si causó que el código salga de manera disruptiva de un código de una manera que dejó un objeto en un estado parcialmente actualizado (y por lo tanto inválido).
supercat
4

Considere un desarrollador que llama a la función C (). No verifica la documentación, por lo que no detecta ninguna excepción. La llamada no es segura.

Es totalmente seguro No necesita detectar excepciones en todos los lugares, solo puede lanzar un try / catch en un lugar donde realmente puede hacer algo útil al respecto. Solo es inseguro si permite que se escape de un hilo, pero generalmente no es difícil evitar que eso suceda.

DeadMG
fuente
Es inseguro ya que no espera que salga una excepción C()y, en consecuencia, no protege contra el código después de que se omita la llamada. Si se requiere ese código para garantizar que alguna invariancia siga siendo cierta, esa garantía se vuelve falsa en la primera excepción que salga C(). No existe un software perfectamente seguro para las excepciones, y ciertamente no donde nunca se esperaban excepciones.
cmaster
1
@cmaster Él no espera que salga una excepciónC() es un gran error. El valor predeterminado en C ++ es que cualquier función puede lanzar; requiere un explícito noexcept(true)para decirle al compilador lo contrario. En ausencia de esta promesa, un programador debe asumir que una función puede lanzar.
Sjoerd
1
@cmaster Esa es su propia falta tonta y nada que ver con el autor de C (). La seguridad de excepción es una cosa, él debe aprender al respecto y seguirla. Si llama a una función que no sabe que es la excepción, debería manejar muy bien lo que sucede si se lanza. Si no necesita ninguna excepción, debe encontrar una implementación que sea.
DeadMG
@Sjoerd y DeadMG: Si bien el estándar permite que cualquier función arroje una excepción, eso no significa que los proyectos deben permitir excepciones en su código. Si prohíbe las excepciones de su código, tal como parece estar haciendo el líder del equipo del OP, no tiene que hacer una programación segura de excepciones. Y si intenta convencer a un líder de equipo para que permita excepciones en una base de código que anteriormente estaba libre de excepciones, habrá un montón de C()funciones que se romperán en presencia de excepciones. Esto es realmente algo muy imprudente, está condenado a generar muchos problemas.
cmaster el
@cmaster Se trata de un nuevo proyecto: vea el primer comentario sobre la respuesta aceptada. Por lo tanto, no hay funciones existentes que se rompan, ya que no hay funciones existentes.
Sjoerd
3

Si está creando un sistema crítico, considere seguir los consejos de su líder de equipo y no use excepciones. Esta es la regla AV 208 en los estándares de codificación C ++ de vehículos aéreos de combate conjunto de Lockheed Martin . Por otro lado, las pautas de MISRA C ++ tienen reglas muy específicas sobre cuándo las excepciones pueden y no pueden usarse si está creando un sistema de software compatible con MISRA.

Si está creando sistemas críticos, es probable que también esté ejecutando herramientas de análisis estático. Muchas herramientas de análisis estático le avisarán si no verifica el valor de retorno de un método, lo que hace que los casos de falta de manejo de errores sean evidentes. Que yo sepa, el soporte de herramientas similares para detectar el manejo adecuado de excepciones no es tan fuerte.

En última instancia, diría que el diseño por contrato y programación defensiva, junto con el análisis estático, es más seguro para los sistemas de software críticos que las excepciones.

Thomas Owens
fuente