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
- Argumento (s) de saída
- 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
- Indicador
- Código
- Objeto
- 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.