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.
Respostas:
Darei a você minha perspectiva, que está codificada no projeto deal.II que você faz referência.
Primeiro, existem dois tipos de condições de erro: Erros que podem ser recuperados e erros que não podem ser recuperados.
O primeiro é, por exemplo, se um arquivo de entrada não pode ser lido - por exemplo, se você está lendo informações de um arquivo como
$HOME/.dealii
esse que pode ou não existir. A função de leitura deve retornar à função de chamada para que ela descubra o que fazer. Também pode ser que um recurso não esteja disponível no momento, mas esteja novamente em um minuto (um sistema de arquivos montado remotamente).O último é, por exemplo, se você estiver tentando adicionar um vetor de tamanho 10 a um vetor de tamanho 20: Tente o quanto puder, não há nada que possa ser feito sobre isso - há um erro no código que levou a o ponto em que tentamos fazer a adição.
Essas duas condições devem ser tratadas de maneira diferente, independentemente da linguagem de programação que você está usando:
No segundo caso, como não há recurso, encerre o programa. Você pode fazer isso lançando uma exceção ou retornando um código de erro que indica ao chamador que nada pode ser feito, mas é possível abortar o programa imediatamente, pois isso facilita muito o programador para depurar o problema.
No primeiro caso, surgiu uma situação excepcional que poderia ser tratada. Embora C e Fortran não tivessem meios de expressar isso, todas as linguagens razoáveis que vieram mais tarde incorporaram maneiras no padrão de linguagem para lidar com retornos "excepcionais", fornecendo, bem, "exceções". Use-os - é para isso que eles servem; eles também são projetados de tal maneira que você não pode esquecer de ignorá-los (se o fizer, a exceção propaga um nível mais alto).
Em outras palavras, o que estou defendendo aqui (e o que o acordo.II faz) é uma mistura de suas estratégias 3 e 5, dependendo do contexto. É verdade que o 3 não funciona em idiomas como C ou Fortran - nesse caso, pode-se argumentar que esse é um bom motivo para não usar idiomas que dificultam a expressão do que você deseja fazer.
Assert
uma exceção ao invés de chamarabort()
.)fonte
std::exception
e elas podem ser capturadas por referência sem conhecer o tipo derivado.