Existen muchas filosofías en diferentes disciplinas de ingeniería de software sobre cómo las bibliotecas deben hacer frente a errores u otras condiciones excepcionales. Algunos de los que he visto:
- Devuelve un código de error con el resultado devuelto por un argumento puntero. Esto es lo que hace PETSc.
- Devuelve errores por un valor centinela. Por ejemplo, malloc devuelve NULL si no puede asignar memoria,
sqrt
devolverá NaN si pasa un número negativo, etc. Este enfoque se utiliza en muchas funciones de libc. - Lanzar excepciones. Utilizado en deal.II, Trilinos, etc.
- Devuelve un tipo de variante; por ejemplo, una función de C ++ que devuelve un objeto de tipo
Result
si se ejecuta correctamente y usa un tipoError
para describir cómo volvería a fallarstd::variant<Error, Result>
. - Utilice afirmar y bloquearse. Usado en p4est y algunas partes de igraph.
Problemas con cada enfoque:
- La comprobación de cada error introduce muchos códigos adicionales. Los valores en los que se almacenará un resultado siempre deben declararse primero, introduciendo muchas variables temporales que solo pueden usarse una vez. Este enfoque explica qué error ocurrió, pero puede ser difícil determinar por qué o, para una pila de llamadas profundas, dónde.
- El caso de error es fácil de ignorar. Además de eso, muchas funciones ni siquiera pueden tener un valor centinela significativo si todo el rango de tipos de salida es un resultado plausible. Muchos de los mismos problemas que # 1.
- Solo es posible en C ++, Python, etc., no en C o Fortran. Se puede imitar en C usando la brujería setjmp / longjmp o libunwind .
- Solo es posible en C ++, Rust, OCaml, etc., no en C o Fortran. Se puede imitar en C usando macro brujería.
- Posiblemente el más informativo. Pero si adopta este enfoque para, por ejemplo, una biblioteca C para la que luego escribe un contenedor de Python, un error tonto como pasar un índice fuera de los límites a una matriz bloqueará el intérprete de Python.
Gran parte de los consejos en Internet sobre el manejo de errores se escriben desde el punto de vista de los sistemas operativos, el desarrollo integrado o las aplicaciones web. Los bloqueos son inaceptables y debe preocuparse por la seguridad. Las aplicaciones científicas no tienen estos problemas en la misma medida, si es que lo hacen.
Otra consideración es qué tipos de errores son recuperables o no. Un error de malloc no es recuperable y, en cualquier caso, el asesino de falta de memoria del sistema operativo lo resolverá antes que usted. Un índice fuera de los límites para un tamaño de matriz tampoco es recuperable. Para mí, como usuario, lo mejor que puede hacer una biblioteca es bloquearse con un mensaje de error informativo. Por otro lado, la falla de, por ejemplo, un solucionador lineal iterativo para converger podría recuperarse usando un solucionador de factorización directa.
¿Cómo deberían las bibliotecas científicas informar errores y esperar que se manejen? Por supuesto, me doy cuenta de que depende del idioma en que se implemente la biblioteca. Pero, por lo que puedo decir, para cualquier biblioteca lo suficientemente útil, la gente querrá llamarlo desde otro idioma que no sea el que está implementado.
Como comentario aparte, creo que el enfoque n. ° 5 se puede mejorar sustancialmente para una biblioteca C si define un puntero de función de controlador de aserción global como parte de la API pública. El controlador de aserción sería predeterminado para informar el número de archivo / línea y el bloqueo. Los enlaces de C ++ para esta biblioteca definirían un nuevo controlador de aserciones que en su lugar arroja una excepción de C ++. Del mismo modo, los enlaces de Python definirían un controlador de aserciones que usa la API de CPython para lanzar una excepción de Python. Pero no conozco ningún ejemplo que tome este enfoque.
Respuestas:
Le daré mi perspectiva, que está codificada en el acuerdo. II proyecto al que hace referencia.
Primero, hay dos tipos de condiciones de error: errores que se pueden recuperar y errores que no se pueden recuperar.
El primero es, por ejemplo, si un archivo de entrada no se puede leer, por ejemplo, si está leyendo información de un archivo como
$HOME/.dealii
ese puede o no existir. La función de lectura debería volver a la función de llamada para que esta última descubra qué hacer. También puede ser que un recurso no esté disponible en este momento, pero puede estar nuevamente en un minuto (un sistema de archivos montado de forma remota).La última es, por ejemplo, si está tratando de agregar un vector de tamaño 10 a un vector de tamaño 20: intente lo que pueda, no hay nada que se pueda hacer al respecto: hay un error en el código que condujo a El punto donde intentamos hacer la adición.
Estas dos condiciones deben tratarse de manera diferente, independientemente del lenguaje de programación que esté utilizando:
En el segundo caso, dado que no hay recurso, finalice el programa. Puede hacerlo lanzando una excepción o devolviendo un código de error que le indica a la persona que llama que no se puede hacer nada, pero también podría abortar el programa de inmediato, ya que eso hace que sea mucho más fácil para el programador depurar el problema.
En el primer caso, ha surgido una situación excepcional que podría manejarse. A pesar de que C y Fortran no tenían medios para expresar esto, todos los lenguajes razonables que vinieron más tarde han incorporado formas en el estándar del lenguaje para tratar tales retornos "excepcionales" al proporcionar, bueno, "excepciones". Úselos: para eso están allí; También están diseñados de tal manera que no puede olvidarse de ignorarlos (si lo hace, la excepción solo propaga un nivel más alto).
En otras palabras, lo que estoy defendiendo aquí (y lo que hace deal.II) es una mezcla de sus estrategias 3 y 5, según el contexto. Es cierto que 3 no funciona en lenguajes como C o Fortran, en cuyo caso se puede argumentar que esa es una buena razón para no usar lenguajes que dificultan la expresión de lo que quieres hacer.
Assert
lanzar una excepción en lugar de llamarabort()
).fuente
std::exception
, y estos pueden ser capturados por referencia sin conocer el tipo derivado.