Me topé con estas preguntas y respuestas varias veces y quería aportar una respuesta más completa. Creo que la mejor manera de pensar en esto es cómo devolver los errores a la persona que llama y lo que devuelve.
Cómo
Hay 3 formas de devolver información de una función:
- Valor de retorno
- Fuera argumento (s)
- Fuera de banda, que incluye goto no local (setjmp / longjmp), archivo o variables de ámbito global, sistema de archivos, etc.
Valor de retorno
Solo puede devolver el valor es un solo objeto, sin embargo, puede ser un complejo arbitrario. Aquí hay un ejemplo de una función de devolución de error:
enum error hold_my_beer();
Un beneficio de los valores de retorno es que permite el encadenamiento de llamadas para un manejo de errores menos intrusivo:
!hold_my_beer() &&
!hold_my_cigarette() &&
!hold_my_pants() ||
abort();
Esto no solo se trata de legibilidad, sino que también puede permitir el procesamiento de una matriz de punteros de función de manera uniforme.
Fuera argumento (s)
Puede devolver más a través de más de un objeto a través de argumentos, pero las mejores prácticas sugieren mantener bajo el número total de argumentos (por ejemplo, <= 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);
}
Fuera de banda
Con setjmp (), define un lugar y cómo desea manejar un valor int, y transfiere el control a esa ubicación a través de un longjmp (). Ver el uso práctico de setjmp y longjmp en C .
Qué
- Indicador
- Código
- Objeto
- Llamar de vuelta
Indicador
Un indicador de error solo le dice que hay un problema pero nada sobre la naturaleza de dicho problema:
struct foo *f = foo_init();
if(!f) {
/// handle the absence of foo
}
Esta es la forma menos poderosa para que una función comunique el estado de error, sin embargo, es perfecta si la persona que llama no puede responder al error de manera gradual de todos modos.
Código
Un código de error le informa a la persona que llama sobre la naturaleza del problema y puede permitir una respuesta adecuada (de lo anterior). Puede ser un valor de retorno, o como el ejemplo de look_ma () arriba de un argumento de error.
Objeto
Con un objeto de error, la persona que llama puede ser informada sobre problemas arbitrarios complicados. Por ejemplo, un código de error y un mensaje legible por humanos adecuado. También puede informar a la persona que llama que varias cosas salieron mal o un error por elemento al procesar una colección:
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]);
}
En lugar de preasignar la matriz de errores, también puede (re) asignarla dinámicamente según sea necesario, por supuesto.
Llamar de vuelta
La devolución de llamada es la forma más poderosa de manejar errores, ya que puede decirle a la función qué comportamiento le gustaría que ocurriera cuando algo sale mal. Se puede agregar un argumento de devolución de llamada a cada función, o si la personalización solo se requiere por instancia de una estructura 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();
Un beneficio interesante de una devolución de llamada es que se puede invocar varias veces, o ninguna en ausencia de errores en los que no hay sobrecarga en el camino feliz.
Hay, sin embargo, una inversión de control. El código de llamada no sabe si se invocó la devolución de llamada. Como tal, puede tener sentido usar también un indicador.