Error al manejar en código C


152

¿Qué considera "mejor práctica" cuando se trata de errores de manejo de errores de manera consistente en una biblioteca de C.

Hay dos formas en las que he estado pensando:

Siempre devuelve el código de error. Una función típica se vería así:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

El siempre proporciona un enfoque de puntero de error:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

Cuando se usa el primer enfoque, es posible escribir código como este donde la verificación de manejo de errores se coloca directamente en la llamada a la función:

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

Que se ve mejor que el código de manejo de errores aquí.

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

Sin embargo, creo que usar el valor de retorno para devolver datos hace que el código sea más legible. Es obvio que algo se escribió en la variable de tamaño en el segundo ejemplo.

¿Tiene alguna idea de por qué debería preferir alguno de esos enfoques o tal vez mezclarlos o usar algo más? No soy fanático de los estados de error globales, ya que tiende a hacer que el uso de subprocesos múltiples de la biblioteca sea mucho más doloroso.

EDITAR: También sería interesante escuchar ideas específicas de C ++ sobre esto, siempre que no impliquen excepciones, ya que no es una opción para mí en este momento ...


Solo he estado aprendiendo C durante aproximadamente dos semanas, pero la sensación que he adquirido es que los parámetros OUT son el valor de retorno de facto para la mayoría de las funciones, ya que evitan la sobrecarga de retornos de estructuras por valor y mitigan la necesidad de desasignar la memoria ya que la mayoría de las variables están en la pila. Entonces, como no estoy usando "return" para el valor real de la función, soy libre de usarlo para el manejo de errores la mayor parte del tiempo.
Joel Roberts el

Respuestas:


74

Me gusta el error como valor de retorno. Si está diseñando la API y desea utilizar su biblioteca de la forma más sencilla posible, piense en estas adiciones:

  • almacenar todos los posibles estados de error en una enumeración typedef'ed y usarlo en su lib. No solo devuelva entradas o, lo que es peor, mezcle entradas o enumeraciones diferentes con códigos de retorno.

  • Proporciona una función que convierte los errores en algo legible para los humanos. Puede ser simple Solo error-enum in, const char * out.

  • Sé que esta idea hace que el uso de subprocesos múltiples sea un poco difícil, pero sería bueno si el programador de aplicaciones puede establecer una devolución de llamada de error global. De esa manera, podrán poner un punto de interrupción en la devolución de llamada durante las sesiones de búsqueda de errores.

Espero eso ayude.


55
¿Por qué dice que "esta idea hace que el uso de subprocesos múltiples sea un poco difícil"? ¿Qué parte se dificulta con el subprocesamiento múltiple? ¿Puedes dar un ejemplo rápido?
SayeedHussain

1
@crypticcoder Simplemente dicho: se puede invocar una devolución de llamada de error global en cualquier contexto de subproceso. Si solo imprime el error, no tendrá ningún problema. Si intenta corregir los problemas, tendrá que averiguar qué hilo de llamada causó el error, y eso dificulta las cosas.
Nils Pipenbrinck

9
¿Qué sucede si desea comunicar más detalles del error? Por ejemplo, tiene un error de analizador y desea proporcionar el número de línea y la columna del error de sintaxis y una forma de imprimirlo todo muy bien.
panzi

1
@panzi, entonces obviamente debe devolver una estructura (o usar un puntero de salida si la estructura es realmente grande) y tener una función para formatear la estructura como una cadena.
Winger Sendon

Demuestro sus primeras 2 viñetas en el código aquí: stackoverflow.com/questions/385975/error-handling-in-c-code/…
Gabriel Staples

92

He utilizado ambos enfoques, y ambos funcionaron bien para mí. Cualquiera que use, siempre trato de aplicar este principio:

Si los únicos errores posibles son errores de programador, no devuelva un código de error, use afirmaciones dentro de la función.

Una afirmación que valida las entradas comunica claramente lo que la función espera, mientras que demasiadas comprobaciones de errores pueden oscurecer la lógica del programa. Decidir qué hacer para todos los diversos casos de error realmente puede complicar el diseño. ¿Por qué descubrir cómo functionX debería manejar un puntero nulo si en cambio puede insistir en que el programador nunca pase uno?


1
¿Tienes un ejemplo de afirmaciones en C? (Soy muy verde para C)
thomthom

Por lo general, es tan simple como assert(X)donde X es cualquier declaración C válida que desea que sea verdad. ver stackoverflow.com/q/1571340/10396 .
AShelly

14
¡Uf, nunca use afirmaciones en el código de la biblioteca ! Además, no mezcle varios estilos de manejo de errores en una sola pieza de código como lo hicieron otros ...
mirabilos

10
Ciertamente estoy de acuerdo en no mezclar estilos. Tengo curiosidad sobre su razonamiento en afirmaciones. Si la documentación de mi función dice "el argumento X no debe ser NULL" o "Y debe ser miembro de esta enumeración", ¿qué tiene de malo assert(X!=NULL);o assert(Y<enumtype_MAX);? Vea esta respuesta sobre los programadores y la pregunta a la que se vincula para obtener más detalles sobre por qué creo que este es el camino correcto.
AShelly

8
@AShelly El problema con afirma que generalmente no están allí en las versiones de lanzamiento.
Calmarius

29

Hay un buen conjunto de diapositivas del CERT de CMU con recomendaciones sobre cuándo usar cada una de las técnicas comunes de manejo de errores C (y C ++). Una de las mejores diapositivas es este árbol de decisión:

Error al manejar el árbol de decisiones

Yo personalmente cambiaría dos cosas sobre este diagrama de flujo.

Primero, aclararía que a veces los objetos deberían usar valores de retorno para indicar errores. Si una función solo extrae datos de un objeto pero no muta el objeto, entonces la integridad del objeto en sí no está en riesgo y es más apropiado indicar errores al usar un valor de retorno.

En segundo lugar, no siempre es apropiado usar excepciones en C ++. Las excepciones son buenas porque pueden reducir la cantidad de código fuente dedicado al manejo de errores, en su mayoría no afectan las firmas de funciones y son muy flexibles en cuanto a qué datos pueden pasar por la pila de llamadas. Por otro lado, las excepciones pueden no ser la opción correcta por algunas razones:

  1. Las excepciones de C ++ tienen una semántica muy particular. Si no desea esa semántica, las excepciones de C ++ son una mala elección. Una excepción debe ser tratada inmediatamente después de ser lanzada y el diseño favorece el caso donde un error necesitará desenrollar la pila de llamadas unos pocos niveles.

  2. Las funciones de C ++ que lanzan excepciones no se pueden ajustar posteriormente para no lanzar excepciones, al menos no sin pagar el costo total de las excepciones de todos modos. Las funciones que devuelven códigos de error se pueden ajustar para generar excepciones de C ++, lo que las hace más flexibles. C ++ lo newhace bien al proporcionar una variante que no arroja.

  3. Las excepciones de C ++ son relativamente caras, pero este inconveniente es mayoritario para los programas que hacen un uso razonable de las excepciones. Un programa simplemente no debería lanzar excepciones en una ruta de código donde el rendimiento es una preocupación. Realmente no importa qué tan rápido su programa pueda informar un error y salir.

  4. A veces, las excepciones de C ++ no están disponibles. Literalmente, no están disponibles en la implementación de C ++, o las pautas de código de uno los prohíben.


Dado que la pregunta original era sobre un contexto multiproceso, creo que la técnica del indicador de error local (lo que se describe en la respuesta de SirDarius ) se subestimó en las respuestas originales. Es seguro para los hilos, no obliga a que el llamante solucione el error de inmediato y puede agrupar datos arbitrarios que describen el error. La desventaja es que debe ser sostenido por un objeto (o supongo que asociado de alguna manera externamente) y es posiblemente más fácil de ignorar que un código de retorno.


55
Puede notar que los estándares de codificación C ++ de Google todavía dicen que no utilizamos excepciones C ++.
Jonathan Leffler

19

Uso el primer enfoque cada vez que creo una biblioteca. Hay varias ventajas de usar una enumeración typedef'ed como código de retorno.

  • Si la función devuelve una salida más complicada, como una matriz y su longitud, no necesita crear estructuras arbitrarias para regresar.

    rc = func(..., int **return_array, size_t *array_length);
  • Permite un manejo simple y estandarizado de errores.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Permite el manejo simple de errores en la función de biblioteca.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • El uso de una enumeración typedef'ed también permite que el nombre de la enumeración sea visible en el depurador. Esto permite una depuración más fácil sin la necesidad de consultar constantemente un archivo de encabezado. También es útil tener una función para traducir esta enumeración en una cadena.

La cuestión más importante, independientemente del enfoque utilizado, es ser coherente. Esto se aplica a la función y la denominación de argumentos, el ordenamiento de argumentos y el manejo de errores.


9

Usa 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;
}

2
El segundo bloque de código se basa en una versión anterior del código en la página de Francesco Nidito referenciada en la parte superior de la respuesta. El ETRYcódigo ha sido revisado desde que se escribió esta respuesta.
Jonathan Leffler

2
Setjmp es una horrible estrategia de manejo de errores. Es costoso, propenso a errores (con locales cambiados no volátiles que no conservan sus valores cambiados y todo) y pierde recursos si asigna alguno entre las llamadas setjmp y longjmp. Debería poder realizar 30 devoluciones y comprobaciones int-val antes de recuperar el costo de sigjmp / longjmp. La mayoría de las pilas de llamadas no son tan profundas, especialmente si no recurre demasiado a la recursividad (y si lo hace, tiene problemas de rendimiento que no sean el costo de las devoluciones + cheques).
PSkocik

1
Si mallocaste la memoria y luego la lanzaste, la memoria se perderá para siempre. También setjmpes costoso, incluso si no se produce ningún error, consumirá bastante tiempo de CPU y espacio de pila. Al usar gcc para Windows, puede elegir entre diferentes métodos de manejo de excepciones para C ++, uno de ellos se basa setjmpy hace que su código sea hasta un 30% más lento en la práctica.
Mecki

7

Personalmente, prefiero el enfoque anterior (devolver un indicador de error).

Cuando sea necesario, el resultado de la devolución debería indicar que se produjo un error, con otra función que se utiliza para averiguar el error exacto.

En su ejemplo getSize (), consideraría que los tamaños siempre deben ser cero o positivos, por lo que devolver un resultado negativo puede indicar un error, al igual que las llamadas al sistema UNIX.

No puedo pensar en ninguna biblioteca que haya usado que vaya para el último enfoque con un objeto de error pasado como puntero. stdio, etc. todos van con un valor de retorno.


1
Para el registro, una biblioteca que he visto usar este último enfoque es la API de programación Maya. Sin embargo, es una biblioteca de C ++ en lugar de C. Es bastante inconsistente en cómo maneja sus errores y, a veces, el error se pasa como valor de retorno y otras veces pasa el resultado como referencia.
Laserallan

1
no olvides strtod, ok, el último argumento no es solo para indicar errores, sino que también lo hace.
quinmars

7

Cuando escribo programas, durante la inicialización, generalmente desenrollo un hilo para el manejo de errores e inicializo una estructura especial para errores, incluido un bloqueo. Luego, cuando detecto un error, a través de los valores de retorno, ingreso la información de la excepción en la estructura y envío un SIGIO al hilo de manejo de excepciones, luego veo si no puedo continuar la ejecución. Si no puedo, envío un SIGURG al hilo de excepción, que detiene el programa con gracia.


7

Devolver el código de error es el enfoque habitual para el manejo de errores en C.

Pero recientemente también experimentamos con el enfoque del indicador de error saliente.

Tiene algunas ventajas sobre el enfoque del valor de retorno:

  • Puede usar el valor de retorno para propósitos más significativos.

  • Tener que escribir ese parámetro de error le recuerda manejar el error o propagarlo. (Nunca olvides comprobar el valor de retorno de fclose, ¿no?)

  • Si usa un puntero de error, puede pasarlo a medida que llama a las funciones. Si alguna de las funciones lo establece, el valor no se perderá.

  • Al establecer un punto de interrupción de datos en la variable de error, puede detectar dónde se produjo el error primero. Al establecer un punto de interrupción condicional, también puede detectar errores específicos.

  • Facilita la automatización de la comprobación de si maneja todos los errores. La convención de código puede obligarlo a llamar a su puntero de error como erry debe ser el último argumento. Entonces, el script puede coincidir con la cadena y err);luego verificar si es seguida por if (*err. En realidad, en la práctica creamos una macro llamada CER(check err return) y CEG(check err goto). Por lo tanto, no es necesario que lo escriba siempre cuando solo queremos devolver un error, y puede reducir el desorden visual.

Sin embargo, no todas las funciones en nuestro código tienen este parámetro saliente. Este parámetro de salida se usa para casos en los que normalmente lanzaría una excepción.


6

He hecho mucha programación en C en el pasado. Y realmente aprecié el valor de retorno del código de error. Pero tiene varias trampas posibles:

  • Números de error duplicados, esto se puede resolver con un archivo global errors.h.
  • Olvidando verificar el código de error, esto debería resolverse con un cluebat y largas horas de depuración. Pero al final aprenderás (o sabrás que alguien más hará la depuración).

2
El segundo problema puede resolverse mediante un nivel de advertencia del compilador adecuado, un mecanismo de revisión de código adecuado y herramientas analizadoras de código estático.
Ilya

1
También puede trabajar según el principio: si se llama a la función API y no se verifica el valor de retorno, hay un error.
Jonathan Leffler

6

El enfoque de UNIX es muy similar a su segunda sugerencia. Devuelve el resultado o un solo valor "salió mal". Por ejemplo, abrir devolverá el descriptor de archivo en caso de éxito o -1 en caso de error. En caso de falla, también establece errnoun número entero global externo para indicar qué falla ocurrió.

Por lo que vale, Cocoa también ha estado adoptando un enfoque similar. Varios métodos devuelven BOOL y toman un NSError **parámetro, de modo que en caso de falla establecen el error y devuelven NO. Entonces el manejo de errores se ve así:

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

que está entre tus dos opciones :-).



5

Aquí hay un enfoque que creo que es interesante, a la vez que requiere un poco de disciplina.

Esto supone que una variable de tipo identificador es la instancia en la que operan todas las funciones API.

La idea es que la estructura detrás del identificador almacena el error anterior como una estructura con los datos necesarios (código, mensaje ...), y el usuario recibe una función que devuelve un puntero a este objeto de error. Cada operación actualizará el objeto puntiagudo para que el usuario pueda verificar su estado sin siquiera llamar a las funciones. A diferencia del patrón errno, el código de error no es global, lo que hace que el enfoque sea seguro para subprocesos, siempre que cada identificador se use correctamente.

Ejemplo:

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;
}

4

El primer enfoque es mejor en mi humilde opinión:

  • Es más fácil escribir la función de esa manera. Cuando observa un error en el medio de la función, simplemente devuelve un valor de error. En el segundo enfoque, debe asignar el valor de error a uno de los parámetros y luego devolver algo ... pero, ¿qué devolvería? No tiene el valor correcto y no devuelve el valor de error.
  • es más popular, por lo que será más fácil de entender, mantener

4

Definitivamente prefiero la primera solución:

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

lo modificaría ligeramente para:

int size;
MYAPIError rc;

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

Además, nunca mezclaré el valor de retorno legítimo con el error, incluso si actualmente el alcance de la función le permite hacerlo, nunca se sabe en qué dirección se implementará la función en el futuro.

Y si ya estamos hablando sobre el manejo de errores, sugeriría goto Error;como código de manejo de errores, a menos undoque se pueda llamar a alguna función para manejar el manejo de errores correctamente.


3

Lo que podría hacer en lugar de devolver su error y, por lo tanto, prohibirle que devuelva datos con su función, es usar un contenedor para su tipo de devolución:

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

Luego, en la función llamada:

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;
} 

Tenga en cuenta que con el siguiente método, el contenedor tendrá el tamaño de MyType más un byte (en la mayoría de los compiladores), lo cual es bastante rentable; y no tendrá que empujar otro argumento en la pila cuando llame a su función ( returnedSizeo returnedErroren los dos métodos que presentó).


3

Aquí hay un programa simple para demostrar las 2 primeras viñetas de la respuesta de Nils Pipenbrinck aquí .

Sus primeras 2 balas son:

  • almacenar todos los posibles estados de error en una enumeración typedef'ed y usarlo en su lib. No solo devuelva entradas o, lo que es peor, mezcle entradas o enumeraciones diferentes con códigos de retorno.

  • Proporciona una función que convierte los errores en algo legible para los humanos. Puede ser simple Solo error-enum in, const char * out.

Suponga que ha escrito un módulo llamado mymodule. Primero, en mymodule.h, define sus códigos de error basados ​​en enumeración y escribe algunas cadenas de error que corresponden a estos códigos. Aquí estoy usando una matriz de cadenas C ( char *), que solo funciona bien si su primer código de error basado en enumeración tiene el valor 0, y no manipula los números a partir de entonces. Si usa números de código de error con espacios u otros valores iniciales, simplemente tendrá que cambiar de usar una matriz de cadena C asignada (como lo hago a continuación) a usar una función que usa una instrucción switch o if / else if declaraciones para asignar desde códigos de error de enumeración a cadenas C imprimibles (que no demuestro). La decisión es tuya.

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 contiene mi función de mapeo para mapear desde códigos de error de enumeración a cadenas C imprimibles:

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;
}

main.c contiene un programa de prueba para demostrar cómo llamar a algunas funciones e imprimir algunos códigos de error desde ellas:

C Principal

#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;
}

Salida:

Demostración de códigos de error basados ​​en enumeración en código de error C (o C ++)
de mymodule_func1 () = MYMODULE_ERROR_OK
código de error de mymodule_func2 () = MYMODULE_ERROR_INVARG
código de error de mymodule_func3 () = MYMODULE_ERROR_ERRY

Referencias

Puede ejecutar este código usted mismo aquí: https://onlinegdb.com/ByEbKLupS .


2

Además de lo que se ha dicho, antes de devolver su código de error, active una afirmación o diagnóstico similar cuando se devuelva un error, ya que facilitará mucho el rastreo. La forma en que hago esto es tener una afirmación personalizada que aún se compila en el lanzamiento, pero solo se dispara cuando el software está en modo de diagnóstico, con la opción de informar silenciosamente a un archivo de registro o pausar en la pantalla.

Yo personalmente devuelvo códigos de error como enteros negativos con no_error como cero, pero te deja con el posible error siguiente

if (MyFunc())
 DoSomething();

Una alternativa es tener un error siempre devuelto como cero y usar una función LastError () para proporcionar detalles del error real.


2

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:

  1. Valor de retorno
  2. Fuera argumento (s)
  3. 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é

  1. Indicador
  2. Código
  3. Objeto
  4. 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.


1

EDITAR: si solo necesita acceder al último error y no trabaja en un entorno multiproceso.

Puede devolver solo verdadero / falso (o algún tipo de #define si trabaja en C y no admite variables bool), y tiene un búfer de error global que contendrá el último error:

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
}

De hecho, pero no es C, puede ser proporcionado por el sistema operativo o no. Si está trabajando en sistemas operativos en tiempo real, por ejemplo, no lo tendrá.
Ilya

1

El segundo enfoque permite que el compilador produzca código más optimizado, porque cuando la dirección de una variable se pasa a una función, el compilador no puede mantener su valor en los registros durante las llamadas posteriores a otras funciones. El código de finalización generalmente se usa solo una vez, justo después de la llamada, mientras que los datos "reales" devueltos por la llamada pueden usarse con más frecuencia


1

Prefiero el manejo de errores en C usando la siguiente 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);
}

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


1
Buena técnica Me parece aún más ordenado con goto's en lugar de repetidos if'. Referencias: una , dos .
Ant_222

0

Además de las otras excelentes respuestas, le sugiero que intente separar el indicador de error y el código de error para guardar una línea en cada llamada, es decir:

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

Cuando tiene muchas comprobaciones de errores, esta pequeña simplificación realmente ayuda.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.