O que você considera "prática recomendada" quando se trata de tratar erros de maneira consistente em uma biblioteca C.
Há duas maneiras em que estive pensando:
Sempre retorne o código de erro. Uma função típica seria assim:
MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);
O sempre fornece uma abordagem de ponteiro de erro:
int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);
Ao usar a primeira abordagem, é possível escrever um código como este, onde a verificação de tratamento de erros é diretamente colocada na chamada de função:
int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
// Error handling
}
Que parece melhor do que o código de manipulação de erros aqui.
MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
// Error handling
}
No entanto, acho que usar o valor de retorno para retornar dados torna o código mais legível. É óbvio que algo foi gravado na variável size no segundo exemplo.
Você tem alguma idéia de por que devo preferir qualquer uma dessas abordagens ou talvez as misture ou use outra coisa? Eu não sou fã de estados de erro globais, uma vez que tende a tornar o uso multiencadeado da biblioteca muito mais doloroso.
EDIT: C ++ idéias específicas sobre isso também seria interessante ouvir, desde que não envolvam exceções, pois não é uma opção para mim no momento ...
fonte
Respostas:
Eu gosto do erro como forma de valor de retorno. Se você estiver projetando a API e quiser usar a sua biblioteca o mais simples possível, pense nas seguintes adições:
armazene todos os estados de erro possíveis em uma enumeração digitada e use-a na sua biblioteca. Não basta retornar ints ou, pior ainda, misturar ints ou enumerações diferentes com códigos de retorno.
fornecer uma função que converta erros em algo legível por humanos. Pode ser simples. Apenas erro-enum in, const char * out.
Sei que essa idéia dificulta um pouco o uso de multithread, mas seria bom se o programador de aplicativos pudesse definir um retorno de chamada de erro global. Dessa forma, eles poderão colocar um ponto de interrupção no retorno de chamada durante as sessões de busca de erros.
Espero que ajude.
fonte
Eu usei as duas abordagens e ambas funcionaram bem para mim. Qualquer que eu use, sempre tento aplicar este princípio:
Se os únicos erros possíveis são erros de programadores, não retorne um código de erro, use asserts dentro da função.
Uma asserção que valida as entradas comunica claramente o que a função espera, enquanto muita verificação de erro pode obscurecer a lógica do programa. Decidir o que fazer para todos os vários casos de erro pode realmente complicar o design. Por que descobrir como o functionX deve lidar com um ponteiro nulo se você pode insistir que o programador nunca passa por um?
fonte
assert(X)
onde X é qualquer instrução C válida que você deseja que seja verdadeira. consulte stackoverflow.com/q/1571340/10396 .assert(X!=NULL);
ouassert(Y<enumtype_MAX);
? Veja esta resposta sobre programadores e a pergunta a que ele se liga para obter mais detalhes sobre por que acho que esse é o caminho certo a seguir.Há um bom conjunto de slides do CERT da CMU com recomendações sobre quando usar cada uma das técnicas comuns de tratamento de erros em C (e C ++). Um dos melhores slides é essa árvore de decisão:
Eu pessoalmente mudaria duas coisas sobre esse carro de fluxo.
Primeiro, eu esclareceria que, às vezes, os objetos devem usar valores de retorno para indicar erros. Se uma função extrai apenas dados de um objeto, mas não o modifica, a integridade do objeto em si não corre risco, e a indicação de erros usando um valor de retorno é mais apropriada.
Segundo, nem sempre é apropriado usar exceções no C ++. As exceções são boas porque podem reduzir a quantidade de código-fonte dedicado ao tratamento de erros, na maioria das vezes não afetam as assinaturas de funções e são muito flexíveis em quais dados podem passar pelo pilha de chamadas. Por outro lado, as exceções podem não ser a escolha certa por alguns motivos:
As exceções do C ++ têm semânticas muito particulares. Se você não deseja essas semânticas, as exceções do C ++ são uma má escolha. Uma exceção deve ser tratada imediatamente após ser lançada, e o design favorece o caso em que um erro precisará desanuviar a pilha de chamada alguns níveis.
As funções C ++ que lançam exceções não podem ser agrupadas posteriormente para não lançar exceções, pelo menos não sem pagar o custo total das exceções de qualquer maneira. As funções que retornam códigos de erro podem ser agrupadas para gerar exceções em C ++, tornando-as mais flexíveis. O C ++
new
acerta isso ao fornecer uma variante não lançadora.As exceções de C ++ são relativamente caras, mas essa desvantagem é exagerada em programas que fazem bom uso de exceções. Um programa simplesmente não deve lançar exceções em um caminho de código em que o desempenho é uma preocupação. Realmente não importa a rapidez com que seu programa possa relatar um erro e sair.
Às vezes, as exceções do C ++ não estão disponíveis. Eles literalmente não estão disponíveis na implementação de C ++, ou as diretrizes de código as proíbem.
Desde a pergunta original era sobre um contexto de vários segmentos, eu acho que a técnica indicador de erro local (o que está descrito no SirDarius 's resposta ) foi subestimado nas respostas originais. É seguro para threads, não força o erro a ser tratado imediatamente pelo chamador e pode agrupar dados arbitrários que descrevem o erro. A desvantagem é que ele deve ser mantido por um objeto (ou suponho, de alguma forma, associado externamente) e é sem dúvida mais fácil de ignorar do que um código de retorno.
fonte
Eu uso a primeira abordagem sempre que crio uma biblioteca. Existem várias vantagens em usar uma enumeração digitada como um código de retorno.
Se a função retornar uma saída mais complicada, como uma matriz, e seu comprimento, você não precisará criar estruturas arbitrárias para retornar.
Ele permite um tratamento simples e padronizado de erros.
Permite o tratamento simples de erros na função de biblioteca.
O uso de uma enumeração digitada também permite que o nome da enumeração fique visível no depurador. Isso permite uma depuração mais fácil, sem a necessidade de consultar constantemente um arquivo de cabeçalho. Ter uma função para converter esse enum em uma string também é útil.
A questão mais importante, independentemente da abordagem usada, deve ser consistente. Isso se aplica à função e nomeação de argumentos, ordenação de argumentos e manipulação de erros.
fonte
Use setjmp .
http://en.wikipedia.org/wiki/Setjmp.h
http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html
http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html
fonte
ETRY
código foi revisado desde que esta resposta foi escrita.setjmp
é caro, mesmo que nenhum erro seja gerado, ele consumirá bastante tempo de CPU e espaço de pilha. Ao usar o gcc para Windows, você pode escolher entre diferentes métodos de tratamento de exceção para o C ++, um deles se baseiasetjmp
e torna seu código até 30% mais lento na prática.Pessoalmente, prefiro a abordagem anterior (retornando um indicador de erro).
Onde necessário, o resultado do retorno deve apenas indicar que ocorreu um erro, com outra função sendo usada para descobrir o erro exato.
No seu exemplo getSize (), considero que os tamanhos sempre devem ser zero ou positivos, portanto, retornar um resultado negativo pode indicar um erro, assim como as chamadas do sistema UNIX.
Não consigo pensar em nenhuma biblioteca que usei para a última abordagem com um objeto de erro passado como um ponteiro.
stdio
, etc, todos vão com um valor de retorno.fonte
Quando escrevo programas, durante a inicialização, normalmente desativo um thread para tratamento de erros e inicializo uma estrutura especial para erros, incluindo um bloqueio. Então, quando detecto um erro, através dos valores retornados, insiro as informações da exceção na estrutura e envio um SIGIO para o segmento de manipulação de exceções, e depois vejo se não consigo continuar a execução. Se não puder, envio um SIGURG para o segmento de exceção, que interrompe o programa normalmente.
fonte
O retorno do código de erro é a abordagem usual para o tratamento de erros em C.
Mas recentemente experimentamos também a abordagem do ponteiro de erro de saída.
Tem algumas vantagens sobre a abordagem do valor de retorno:
Você pode usar o valor de retorno para fins mais significativos.
Ter que escrever esse parâmetro de erro lembra para lidar com o erro ou propagá-lo. (Você nunca esquece de verificar o valor de retorno
fclose
, não é?)Se você usar um ponteiro de erro, poderá transmiti-lo ao chamar funções. Se alguma das funções o definir, o valor não será perdido.
Ao definir um ponto de interrupção de dados na variável de erro, você pode descobrir onde ocorreu o erro primeiro. Ao definir um ponto de interrupção condicional, você também pode detectar erros específicos.
Torna mais fácil automatizar a verificação se você lida com todos os erros. A convenção de código pode forçá-lo a chamar seu ponteiro de erro
err
e deve ser o último argumento. Para que o script possa corresponder à stringerr);
e verifique se é seguido porif (*err
. Na prática, fizemos uma macro chamadaCER
(check err return) eCEG
(check err goto). Portanto, você não precisa digitá-lo sempre quando queremos retornar o erro e reduzir a confusão visual.Porém, nem todas as funções em nosso código possuem esse parâmetro de saída. Esse parâmetro de saída é usado para casos em que você normalmente lançaria uma exceção.
fonte
Eu fiz muita programação C no passado. E eu realmente apreciei o valor de retorno do código de erro. Mas há várias possíveis armadilhas:
fonte
A abordagem UNIX é mais semelhante à sua segunda sugestão. Retorne o resultado ou um único valor "deu errado". Por exemplo, abrir retornará o descritor de arquivo em caso de sucesso ou -1 em caso de falha. Na falha, ele também define
errno
um número inteiro global externo para indicar qual falha ocorreu.Pelo que vale, o cacau também tem adotado uma abordagem semelhante. Vários métodos retornam BOOL e usam um
NSError **
parâmetro, para que, em caso de falha, eles definam o erro e retornem NO. Em seguida, o tratamento de erros se parece com:que está entre as duas opções :-).
fonte
Também estava pensando sobre esse problema recentemente e escrevi algumas macros para C que simulam a semântica de try-catch-finalmente usando valores de retorno puramente locais. Espero que você ache útil.
fonte
Aqui está uma abordagem que eu acho interessante, enquanto requer alguma disciplina.
Isso pressupõe que uma variável do tipo identificador é a instância na qual operam todas as funções da API.
A idéia é que a estrutura por trás do identificador armazene o erro anterior como uma estrutura com os dados necessários (código, mensagem ...), e o usuário recebe uma função que retorna um ponteiro para esse objeto de erro. Cada operação atualiza o objeto apontado para que o usuário possa verificar seu status sem precisar chamar funções. Ao contrário do padrão errno, o código de erro não é global, o que torna a abordagem segura para threads, desde que cada identificador seja usado corretamente.
Exemplo:
fonte
Primeira abordagem é melhor IMHO:
fonte
Definitivamente, prefiro a primeira solução:
eu o modificaria levemente, para:
Além disso, nunca misturarei valor de retorno legítimo com erro, mesmo que atualmente o escopo da função permita que você faça isso, você nunca sabe para que lado a implementação da função será no futuro.
E se já estamos falando sobre tratamento de erros, eu sugeriria
goto Error;
como código de tratamento de erros, a menos que algumaundo
função possa ser chamada para lidar com o tratamento de erros corretamente.fonte
O que você poderia fazer em vez de retornar seu erro e, assim, proibi-lo de retornar dados com sua função, está usando um wrapper para seu tipo de retorno:
Então, na função chamada:
Observe que, com o método a seguir, o wrapper terá o tamanho do MyType mais um byte (na maioria dos compiladores), o que é bastante lucrativo; e você não precisará pressionar outro argumento na pilha quando chamar sua função (
returnedSize
oureturnedError
nos dois métodos apresentados).fonte
Aqui está um programa simples para demonstrar as 2 primeiras balas da resposta de Nils Pipenbrinck aqui .
Suas duas primeiras balas são:
Suponha que você tenha escrito um módulo chamado
mymodule
. Primeiro, em mymodule.h, você define seus códigos de erro baseados em enum e escreve algumas seqüências de erros que correspondem a esses códigos. Aqui, estou usando uma matriz de C strings (char *
), que só funciona bem se o seu primeiro código de erro baseado em enum tiver o valor 0 e você não manipular os números posteriormente. Se você usa números de código de erro com intervalos ou outros valores iniciais, basta mudar de usar uma matriz de cordas C mapeada (como eu faço abaixo) para usar uma função que usa uma instrução switch ou if / else if para mapear de códigos de erro enum para seqüências C imprimíveis (que eu não demonstro).A escolha é sua.mymodule.h
mymodule.c contém minha função de mapeamento para mapear de códigos de erro de enumeração para cadeias C imprimíveis:
mymodule.c
O main.c contém um programa de teste para demonstrar a chamada de algumas funções e a impressão de alguns códigos de erro:
main.c
Resultado:
Referências:
Você pode executar esse código aqui: https://onlinegdb.com/ByEbKLupS .
fonte
Além do que foi dito, antes de retornar seu código de erro, inicie um diagnóstico afirmativo ou semelhante quando um erro for retornado, pois isso facilitará muito o rastreamento. A maneira como faço isso é ter uma declaração personalizada que ainda é compilada no lançamento, mas só é acionada quando o software está no modo de diagnóstico, com a opção de relatar silenciosamente em um arquivo de log ou pausar na tela.
Pessoalmente, retorno códigos de erro como números inteiros negativos com no_error como zero, mas isso deixa você com o possível bug a seguir
Uma alternativa é ter uma falha sempre retornada como zero e usar uma função LastError () para fornecer detalhes do erro real.
fonte
Eu me deparei com essas perguntas e respostas várias vezes e queria contribuir com uma resposta mais abrangente. Eu acho que a melhor maneira de pensar sobre isso é como retornar erros ao chamador e o que você retorna.
Quão
Existem três maneiras de retornar informações de uma função:
Valor de retorno
Você só pode retornar valor é um único objeto, no entanto, pode ser um complexo arbitrário. Aqui está um exemplo de erro ao retornar a função:
Um benefício dos valores de retorno é que ele permite o encadeamento de chamadas para tratamento de erros menos invasivo:
Isso não é apenas sobre legibilidade, mas também pode permitir o processamento de uma matriz de tais indicadores de função de maneira uniforme.
Argumento (s) de saída
Você pode retornar mais por meio de mais de um objeto por meio de argumentos, mas as práticas recomendadas sugerem manter o número total de argumentos baixo (por exemplo, <= 4):
Fora da banda
Com setjmp (), você define um local e como deseja manipular um valor int e transfere o controle para esse local por meio de um longjmp (). Veja uso prático de setjmp e longjmp em C .
o que
Indicador
Um indicador de erro informa apenas que há um problema, mas nada sobre a natureza do problema:
Essa é a maneira menos poderosa de uma função comunicar o estado do erro, no entanto, é perfeita se o chamador não puder responder ao erro de qualquer maneira graduada.
Código
Um código de erro informa ao chamador sobre a natureza do problema e pode permitir uma resposta adequada (acima). Pode ser um valor de retorno ou como o exemplo look_ma () acima de um argumento de erro.
Objeto
Com um objeto de erro, o chamador pode ser informado sobre questões complicadas arbitrárias. Por exemplo, um código de erro e uma mensagem legível por humanos adequada. Também pode informar ao chamador que várias coisas deram errado ou um erro por item ao processar uma coleção:
Em vez de pré-alocar a matriz de erros, você também pode (re) alocá-la dinamicamente conforme necessário, é claro.
Ligue de volta
O retorno de chamada é a maneira mais poderosa de lidar com erros, pois você pode dizer à função qual comportamento gostaria que acontecesse quando algo desse errado. Um argumento de retorno de chamada pode ser adicionado a cada função ou se a personalização for necessária apenas por instância de uma estrutura como esta:
Um benefício interessante de um retorno de chamada é que ele pode ser chamado várias vezes, ou nenhum, na ausência de erros nos quais não há sobrecarga no caminho feliz.
Há, no entanto, uma inversão de controle. O código de chamada não sabe se o retorno de chamada foi chamado. Como tal, pode fazer sentido também usar um indicador.
fonte
EDIT: Se você precisar acessar apenas o último erro e não trabalhar em ambiente multithread.
Você pode retornar apenas verdadeiro / falso (ou algum tipo de #define se você trabalha em C e não suporta variáveis booleanas) e possui um buffer de erro global que conterá o último erro:
fonte
A segunda abordagem permite que o compilador produza código mais otimizado, porque quando o endereço de uma variável é passado para uma função, o compilador não pode manter seu valor no (s) registrador (es) durante chamadas subseqüentes a outras funções. O código de conclusão geralmente é usado apenas uma vez, logo após a chamada, enquanto os dados "reais" retornados da chamada podem ser usados com mais frequência
fonte
Prefiro o tratamento de erros em C usando a seguinte técnica:
Fonte: http://blog.staila.com/?p=114
fonte
goto
's' em vez deif
's repetidos . Referências: um , dois .Além das outras ótimas respostas, sugiro que você tente separar o sinalizador e o código do erro para salvar uma linha em cada chamada, ou seja:
Quando você tem muita verificação de erros, essa pequena simplificação realmente ajuda.
fonte