Tratamento de erros no código C

152

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 ...

Laserallan
fonte
Eu só aprendi C há cerca de duas semanas, mas a sensação que obtive é que os parâmetros OUT são o valor de retorno defacto para a maioria das funções, pois evitam a sobrecarga de retornar estruturas por valor e mitigam a necessidade de desalocar a memória, pois a maioria das variáveis ​​está na pilha. Portanto, como não estou usando "return" para o valor real da função, estou livre para usá-lo na manipulação de erros a maior parte do tempo.
Joel Roberts

Respostas:

74

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.

Nils Pipenbrinck
fonte
5
Por que você diz "essa idéia dificulta um pouco o uso de vários segmentos". Qual parte é dificultada pelo multi-threading? Você pode dar um exemplo rápido?
SayeedHussain
1
@crypticcoder Simplesmente disse: um retorno de chamada de erro global pode ser chamado em qualquer contexto de thread. Se você apenas imprimir o erro, não terá problemas. Se você tentar corrigir problemas, precisará descobrir qual thread de chamada causou o erro e isso dificulta as coisas.
Nils Pipenbrinck
9
E se você deseja comunicar mais detalhes do erro? Por exemplo, você tem um erro no analisador e deseja fornecer o número da linha e a coluna do erro de sintaxe, além de uma maneira de imprimir tudo corretamente.
panzi
1
@panzi, obviamente você precisa retornar uma estrutura (ou usar um ponteiro de saída se estrutura for realmente grande) e ter uma função para formatar a estrutura como uma string.
Winger Sendon
I demonstrar suas 2 primeiras balas no código aqui: stackoverflow.com/questions/385975/error-handling-in-c-code/...
Gabriel Staples
92

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?

AShelly
fonte
1
Tem um exemplo de afirmações em C? (Eu sou muito verde em relação a C) #
28413
Geralmente é tão simples quanto assert(X)onde X é qualquer instrução C válida que você deseja que seja verdadeira. consulte stackoverflow.com/q/1571340/10396 .
AShelly
14
Ugh, nunca use afirmações no código da biblioteca ! Além disso, não se misturam vários estilos de manipulação de erro em um pedaço de código como outros fizeram ...
mirabilos
10
Eu certamente concordo em não misturar estilos. Estou curioso sobre o seu raciocínio sobre afirmações. Se a documentação da minha função diz "argumento X não deve ser NULL" ou "Y deve ser um membro dessa enumeração", então o que há de errado com assert(X!=NULL);ou assert(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.
precisa saber é o seguinte
8
@ AShelly O problema com afirma que eles geralmente não existem nas versões de lançamento.
Calmarius
29

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:

Árvore de decisão de tratamento de erros

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:

  1. 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.

  2. 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 ++ newacerta isso ao fornecer uma variante não lançadora.

  3. 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.

  4. À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.

Praxeolitic
fonte
5
Você pode observar que os padrões de codificação C ++ do Google ainda dizem que não usamos exceções em C ++.
Jonathan Leffler
19

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.

    rc = func(..., int **return_array, size_t *array_length);
  • Ele permite um tratamento simples e padronizado de erros.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Permite o tratamento simples de erros na função de biblioteca.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • 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.

Jeffrey Cohen
fonte
9

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

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}
Amir Saniyan
fonte
2
O segundo bloco de código é baseado em uma versão anterior do código na página de Francesco Nidito, mencionada na parte superior da resposta. O ETRYcódigo foi revisado desde que esta resposta foi escrita.
precisa saber é o seguinte
2
Setjmp é uma horrível estratégia de manipulação de erros. É caro, propenso a erros (com locais alterados não voláteis, sem reter seus valores alterados e tudo) e vaza recursos se você alocar algum entre as chamadas setjmp e longjmp. Você deve conseguir 30 retornos e verificações int-val antes de recuperar o custo de sigjmp / longjmp. A maioria das pilhas de chamadas não é tão profunda, especialmente se você não recorre muito à recursão (e se o fizer, terá problemas de desempenho diferentes do custo dos retornos + dos cheques).
PSKocik 23/06/19
1
Se você alocar memória e depois jogá-la, a memória vazará para sempre. Também 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 baseia setjmpe torna seu código até 30% mais lento na prática.
Mecki
7

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.

Alnitak
fonte
1
Para o registro, uma biblioteca que eu vi usar a última abordagem é a API de programação do Maya. É uma biblioteca c ++ em vez de C, no entanto. É bastante inconsistente na maneira como ele lida com seus erros e, às vezes, o erro é passado como valor de retorno e outras vezes passa o resultado como referência.
Laserallan
1
não esqueça strtod, ok, o último argumento não é apenas para indicar erros, mas também o faz.
quinmars 22/12/08
7

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.

Henry
fonte
7

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 erre deve ser o último argumento. Para que o script possa corresponder à string err);e verifique se é seguido por if (*err. Na prática, fizemos uma macro chamada CER(check err return) e CEG(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.

Calmarius
fonte
6

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:

  • Números de erro duplicados, isso pode ser resolvido com um arquivo errors.h global.
  • Esquecendo de verificar o código de erro, isso deve ser resolvido com um cluebat e longas horas de depuração. Mas no final você aprenderá (ou saberá que outra pessoa fará a depuração).
Toon Krijthe
fonte
2
O segundo problema pode ser resolvido pelo nível adequado de aviso do compilador, pelo mecanismo adequado de revisão de código e pelas ferramentas estáticas do analisador de código.
Ilya
1
Você também pode trabalhar com o princípio: se a função API for chamada e o valor de retorno não for verificado, haverá um erro.
Jonathan Leffler
6

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 errnoum 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:

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

que está entre as duas opções :-).


fonte
5

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:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}
SirDarius
fonte
4

Primeira abordagem é melhor IMHO:

  • É mais fácil escrever a função dessa maneira. Quando você percebe um erro no meio da função, basta retornar um valor de erro. Na segunda abordagem, você precisa atribuir valor de erro a um dos parâmetros e retornar algo .... mas o que você retornaria - você não tem valor correto e não retorna valor de erro.
  • é mais popular, então será mais fácil entender, manter
Klesk
fonte
4

Definitivamente, prefiro a primeira solução:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

eu o modificaria levemente, para:

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

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 alguma undofunção possa ser chamada para lidar com o tratamento de erros corretamente.

Ilya
fonte
3

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:

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

Então, na função chamada:

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

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 ( returnedSizeou returnedErrornos dois métodos apresentados).

Antoine C.
fonte
3

Aqui está um programa simples para demonstrar as 2 primeiras balas da resposta de Nils Pipenbrinck aqui .

Suas duas primeiras balas são:

  • 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.

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

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

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

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

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

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

Resultado:

Demonstração de códigos de erro baseados em enum em C (ou C ++)
código de erro de mymodule_func1 () = MYMODULE_ERROR_OK
código de erro de mymodule_func2 () = MYMODULE_ERROR_INVARG
código de erro de mymodule_func3 () = MYMODULE_ERROR_MYERROR

Referências:

Você pode executar esse código aqui: https://onlinegdb.com/ByEbKLupS .

Gabriel Staples
fonte
2

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

if (MyFunc())
 DoSomething();

Uma alternativa é ter uma falha sempre retornada como zero e usar uma função LastError () para fornecer detalhes do erro real.

SmacL
fonte
2

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:

  1. Valor de retorno
  2. Argumento (s) de saída
  3. Fora da banda, que inclui goto não local (setjmp / longjmp), variáveis ​​de escopo global ou de arquivo, sistema de arquivos etc.

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:

  enum error hold_my_beer();

Um benefício dos valores de retorno é que ele permite o encadeamento de chamadas para tratamento de erros menos invasivo:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

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):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

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

  1. Indicador
  2. Código
  3. Objeto
  4. Ligue de volta

Indicador

Um indicador de erro informa apenas que há um problema, mas nada sobre a natureza do problema:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

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:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

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:

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

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.

Allan Wind
fonte
1

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:

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}
Igor Oks
fonte
Na verdade, mas não é C, pode ser fornecido pelo sistema operacional ou não. Se você estiver trabalhando em sistemas operacionais em tempo real, por exemplo, não o terá.
Ilya
1

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

dmityugov
fonte
1

Prefiro o tratamento de erros em C usando a seguinte técnica:

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

   // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

   // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

   // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

   // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

   // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

   // good? return list or else return NULL
    return (good ? list : NULL);
}

Fonte: http://blog.staila.com/?p=114

Nitin Kunal
fonte
1
Boa técnica. Acho ainda mais limpo com goto's' em vez de if's repetidos . Referências: um , dois .
Ant_222
0

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:

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

Quando você tem muita verificação de erros, essa pequena simplificação realmente ajuda.

Ant_222
fonte