programacion muestra manejo los lenguaje hacer fisica excepciones errores error dev compilacion como barra aparecer c error-handling

muestra - Manejo de errores en el código C



manejo de excepciones en lenguaje c (20)

¿Qué considera la "mejor práctica" cuando se trata de errores en el manejo de errores de una 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);

Siempre proporciona un enfoque de puntero de error:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

Cuando se utiliza el primer enfoque, es posible escribir código como este donde la verificación de manejo de errores se coloca directamente en la llamada de 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 sobre por qué debería preferir alguno de esos enfoques o quizás mezclarlos o usar algo más? No soy partidario de los estados de error globales ya que tiende a hacer que el uso de múltiples hilos de la biblioteca sea más doloroso.

EDITAR: Las ideas específicas de C ++ sobre esto también serían interesantes de escuchar siempre y cuando no involucren excepciones ya que no es una opción para mí en este momento ...


Además de las otras excelentes respuestas, 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 tienes un montón de comprobación de errores, esta pequeña simplificación realmente ayuda.


Además de lo que se ha dicho, antes de devolver su código de error, ejecute un diagnóstico afirmativo o 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 la versión, pero solo se activa cuando el software está en modo de diagnóstico, con la opción de informar silenciosamente a un archivo de registro o hacer una pausa en la pantalla.

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

if (MyFunc()) DoSomething();

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


Cuando escribo programas, durante la inicialización, por lo general hago girar un hilo para manejar el error e inicializo una estructura especial para errores, incluyendo un bloqueo. Luego, cuando detecto un error, a través de valores devueltos, 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 con la ejecución. Si no puedo, envío un SIGURG al hilo de excepción, que detiene el programa correctamente.


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 te permite hacerlo, nunca se sabe en qué dirección funcionará la implementació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 que se pueda llamar a alguna función de undo para manejar el manejo de errores correctamente.


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 puntero de error saliente.

Tiene algunas ventajas sobre el enfoque del valor de retorno:

  • Puede usar el valor de retorno para fines más significativos.

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

  • Si usa un puntero de error, puede pasarlo mientras llama a 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 ver dónde ocurre el error primero. Al establecer un punto de interrupción condicional, también puede detectar errores específicos.

  • Hace que sea más fácil automatizar el control si maneja todos los errores. La convención del código puede forzarlo a llamar a su puntero de error como err y debe ser el último argumento. Entonces el script puede coincidir con la cadena err); luego verifique si es seguido por if (*err . De hecho, en la práctica, creamos una macro llamada CER (check err return) y CEG (check err goto). Por lo tanto, no necesita escribirla siempre cuando solo queremos regresar por 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 saliente se usa para casos en los que normalmente lanzaría una excepción.


EDITAR: si necesita acceso solo 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 de 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 }


El enfoque de UNIX es muy similar a su segunda sugerencia. Devuelve el resultado o un único 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 errno , un 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 parámetro NSError ** para que, al fallar, establezcan el error y devuelvan NO. Entonces el manejo del error se ve así:

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

que está en algún lugar entre tus dos opciones :-).


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 un 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 del error.
  • es más popular por lo que será más fácil de entender, mantener

El segundo enfoque permite al compilador producir un código más optimizado, ya que cuando la dirección de una variable se pasa a una función, el compilador no puede mantener su valor en registro (s) 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 de la llamada se pueden usar con mayor frecuencia.


Este es un enfoque que creo que es interesante, aunque requiere cierta disciplina.

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

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

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


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

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

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 los errores utilizando 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 a 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 elección correcta por algunas razones:

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

  2. Las funciones de C ++ que arrojan excepciones no pueden envolverse más tarde 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 envolver para lanzar excepciones de C ++, haciéndolas más flexibles. El new C ++ consigue este derecho al proporcionar una variante que no lanza.

  3. Las excepciones de C ++ son relativamente costosas, pero esta desventaja es en su mayoría exagerada para los programas que hacen un uso sensato de las excepciones. Un programa simplemente no debe arrojar excepciones en una ruta de código donde el rendimiento es una preocupación. Realmente no importa qué tan rápido su programa puede informar un error y salir.

  4. Algunas veces las excepciones de C ++ no están disponibles. O no están literalmente disponibles en la implementación C ++ de uno, o las reglas del 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 answer SirDarius ) no se apreció en las respuestas originales. Es inseguro en cuanto a los hilos, no obliga al error a ser tratado inmediatamente por la persona que llama, y ​​puede agrupar datos arbitrarios que describen el error. La desventaja es que debe ser sostenida por un objeto (o supongo que de alguna manera asociada externamente) y es posiblemente más fácil de ignorar que un código de retorno.


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:

  • Duplique los números de error, 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).

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

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

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


Lo que podría hacer en lugar de devolver su error y, por lo tanto, prohibirle que devuelva datos con su función, es utilizar 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 presionar otro argumento en la pila cuando llame a su función ( returnedSize o returnedError en los dos métodos que presentó).


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

  • almacena todos los posibles estados de error en una enumeración typedef''ed y úsala en tu lib. No solo devuelva enteros o incluso cosas peores, mezcle 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 en, 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 forma, 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.


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

Donde sea necesario, el resultado de la devolución debería indicar que ocurrió un error, con otra función que se usa para descubrir el error exacto.

En su ejemplo de 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 he usado que vaya para el último acercamiento con un objeto de error pasado como un puntero. stdio , etc. todos van con un valor de retorno.


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



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


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

  • Si la función devuelve un resultado más complicado, como una matriz y su longitud, no es necesario crear estructuras arbitrarias para devolver.

    rc = func(..., int **return_array, size_t *array_length);

  • Permite un manejo de errores simple y estandarizado.

    if ((rc = func(...)) != API_SUCCESS) { /* Error Handling */ }

  • Permite el manejo simple de errores en la función de la 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 enum sea visible en el depurador. Esto permite una depuración más fácil sin la necesidad de consultar constantemente un archivo de encabezado. Tener una función para traducir esta enumeración en una cadena también es útil.

El problema más importante independientemente del enfoque utilizado es ser coherente. Esto se aplica a nombres de funciones y argumentos, ordenamiento de argumentos y manejo de errores.