Existem muitas filosofias em diferentes disciplinas de engenharia de software sobre como as bibliotecas devem lidar com erros ou outras condições excepcionais. Alguns dos que eu já vi:
- Retorne um código de erro com o resultado retornado por um argumento de ponteiro. É isso que o PETSc faz.
- Retornar erros por um valor sentinela. Por exemplo, malloc retorna NULL se não puder alocar memória,
sqrt
retornará NaN se você passar um número negativo, etc. Essa abordagem é usada em muitas funções libc. - Lance exceções. Usado no negócio. II, Trilinos, etc.
- Retornar um tipo de variante; por exemplo, uma função C ++ que retorna um objeto do tipo
Result
se ele for executado corretamente e usa um tipoError
para descrever como ele falhariastd::variant<Error, Result>
. - Use assert e travar. Usado no p4est e em algumas partes do igraph.
Problemas com cada abordagem:
- A verificação de todos os erros introduz muito código extra. Os valores nos quais um resultado será armazenado sempre precisam ser declarados primeiro, introduzindo muitas variáveis temporárias que podem ser usadas apenas uma vez. Essa abordagem explica qual erro ocorreu, mas pode ser difícil determinar por que ou, para uma pilha profunda de chamadas, onde.
- O caso de erro é fácil de ignorar. Além disso, muitas funções não podem ter um valor sentinela significativo se todo o intervalo de tipos de saída for um resultado plausível. Muitos dos mesmos problemas que o nº 1.
- Só é possível em C ++, Python, etc., não em C ou Fortran. Pode ser imitado em C usando a feitiçaria setjmp / longjmp ou libunwind .
- Somente possível em C ++, Rust, OCaml etc., não em C ou Fortran. Pode ser imitado em C usando macro feitiçaria.
- Indiscutivelmente o mais informativo. Mas se você adotar essa abordagem para, digamos, uma biblioteca C para a qual você escreve um wrapper Python, um erro bobo como passar um índice fora dos limites para uma matriz causará um erro no interpretador Python.
Muitos dos conselhos na internet sobre tratamento de erros são escritos do ponto de vista de sistemas operacionais, desenvolvimento incorporado ou aplicativos da Web. Falhas são inaceitáveis e você precisa se preocupar com segurança. As aplicações científicas não têm esses problemas quase na mesma extensão, se houver.
Outra consideração é que tipos de erros são recuperáveis ou não. Uma falha de malloc não é recuperável e, em qualquer caso, o killer de falta de memória do sistema operacional chegará a ela antes de você. Um índice fora dos limites para um tamanho de matriz também não é recuperável. Para mim, como usuário, a melhor coisa que uma biblioteca pode fazer é travar com uma mensagem de erro informativa. Por outro lado, a falha de, digamos, um solucionador linear iterativo para convergir pode ser recuperada usando um solucionador de fatoração direta.
Como as bibliotecas científicas devem relatar erros e esperar que sejam tratados? Percebo, é claro, que depende do idioma em que a biblioteca está implementada. Mas, tanto quanto posso dizer, para qualquer biblioteca suficientemente útil, as pessoas vão querer chamá-lo de algum idioma diferente daquele em que está implementado.
Como um aparte, acho que a abordagem nº 5 pode ser aprimorada substancialmente para uma biblioteca C se ela definir um ponteiro de função do manipulador de asserção global como parte da API pública. O manipulador de asserções usará como padrão o relatório de número de arquivo / linha e falha. As ligações C ++ para esta biblioteca definiriam um novo manipulador de asserção que, em vez disso, lança uma exceção C ++. Da mesma forma, as ligações Python definiriam um manipulador de asserção que usa a API CPython para gerar uma exceção Python. Mas não conheço exemplos que adotem essa abordagem.