tecnica sikana respirar natacion nadar mejor libre estilo crol como brazada bien c error-handling

sikana - ¿Hay una mejor manera de manejar errores en el estilo C?



tecnica de crol (10)

¿Qué hay de esto?

int NS_Expression(void) { int ok = 1; ok = ok && NS_Term(); ok = ok && Emit("MOVE D0, D1/n"); ok = ok && NS_AddSub(); return ok }

Intento aprender C escribiendo un analizador / analizador simple. Hasta ahora ha sido una experiencia muy enriquecedora, sin embargo, viniendo de una sólida formación en C # estoy teniendo algunos problemas para ajustarme, en particular a la falta de excepciones.

Ahora he leído Cleaner, soy más elegante y más difícil de reconocer y estoy de acuerdo con cada palabra de ese artículo; En mi código C # evito lanzar excepciones siempre que sea posible, sin embargo, ahora que me encuentro ante un mundo en el que no puedo lanzar excepciones, mi manejo de errores está inundando por completo la lógica de mi código, que de otro modo sería limpia y fácil de leer.

En este momento estoy escribiendo un código que necesita fallar rápidamente si hay un problema, y ​​también anida profundamente: he establecido un patrón de manejo de errores mediante el cual las funciones "Obtener" devuelven NULO en caso de error y vuelven otras funciones -1 en falla. En ambos casos, la función que falla llama a NS_SetError() y, por lo tanto, toda la función de llamada que se necesita hacer es limpiar e inmediatamente regresar a un error.

Mi problema es que el número de if (Action() < 0) return -1; Las afirmaciones que hago me las cuento: es muy repetitiva y oscurece por completo la lógica subyacente. Terminé creando una macro simple para tratar de mejorar la situación, por ejemplo:

#define NOT_ERROR(X) if ((X) < 0) return -1 int NS_Expression(void) { NOT_ERROR(NS_Term()); NOT_ERROR(Emit("MOVE D0, D1/n")); if (strcmp(current->str, "+") == 0) { NOT_ERROR(NS_Add()); } else if (strcmp(current->str, "-") == 0) { NOT_ERROR(NS_Subtract()); } else { NS_SetError("Expected: operator"); return -1; } return 0; }

Cada una de las funciones NS_Term , NS_Add y NS_Subtract hacen un NS_SetError() y devuelven -1 en el caso de un error, es mejor , pero todavía se siente como si estuviera abusando de las macros y no permite ninguna limpieza (algunas funciones, en particular Get funciones que devuelven un puntero, son más complejas y requieren que se ejecute el código de limpieza).

En general, parece que me falta algo, a pesar de que el manejo de errores de esta manera es supuestamente más fácil de reconocer. En muchas de mis funciones, realmente estoy luchando por identificar si los errores se están manejando correctamente o no.

  • Algunas funciones devuelven NULL en un error
  • Algunas funciones devuelven < 0 en un error
  • Algunas funciones nunca producen un error
  • Mis funciones hacen un NS_SetError() , pero muchas otras funciones no.

¿Existe una mejor manera de estructurar mis funciones o todos los demás también tienen este problema?

También es tener buenas funciones (que devuelven un puntero a un objeto) devolver NULL en un error, ¿o simplemente confunde mi manejo de errores?


Además de goto , el estándar C tiene otra construcción para manejar un control de flujo excepcional setjmp/longjmp . Tiene la ventaja de que puede salir de declaraciones de control anidadas de forma más fácil que con un break como lo propuso alguien, y además de lo que proporciona goto tiene una indicación de estado que puede codificar el motivo de lo que salió mal.

Otro problema es solo la sintaxis de tu construcción. No es una buena idea usar una declaración de control que se pueda agregar de manera inadvertida. En tu caso

if (bla) NOT_ERROR(X); else printf("wow!/n");

iría fundamentalmente mal. Yo usaría algo como

#define NOT_ERROR(X) / if ((X) >= 0) { (void)0; } / else return -1

en lugar.


Es un problema mayor cuando tiene que repetir el mismo código de finalización antes de cada return de un error. En tales casos, es ampliamente aceptado usar goto :

int func () { if (a() < 0) { goto failure_a; } if (b() < 0) { goto failure_b; } if (c() < 0) { goto failure_c; } return SUCCESS; failure_c: undo_b(); failure_b: undo_a(); failure_a: return FAILURE; }

Incluso puedes crear tus propias macros para ahorrar algo de tipeo, algo como esto (aunque no lo he probado):

#define CALL(funcname, ...) / if (funcname(__VA_ARGS__) < 0) { / goto failure_ ## funcname; / }

En general, es un enfoque mucho más limpio y menos redundante que el manejo trivial:

int func () { if (a() < 0) { return FAILURE; } if (b() < 0) { undo_a(); return FAILURE; } if (c() < 0) { undo_b(); undo_a(); return FAILURE; } return SUCCESS; }

Como una sugerencia adicional, a menudo uso el encadenamiento para reducir el número de if en mi código:

if (a() < 0 || b() < 0 || c() < 0) { return FAILURE; }

Como || es un operador de cortocircuito, lo anterior sustituiría a tres if ''s por separado. Considere usar el encadenamiento en una declaración de return también:

return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;


Una declaración goto es la forma más fácil y potencialmente más limpia de implementar el procesamiento de estilo de excepción. El uso de una macro facilita la lectura si incluye la lógica de comparación dentro de las macroargas. Si organiza las rutinas para realizar un trabajo normal (es decir, sin errores) y solo utiliza las excepciones goto on, está bastante limpio para la lectura. Por ejemplo:

/* Exception macro */ #define TRY_EXIT(Cmd) { if (!(Cmd)) {goto EXIT;} } /* My memory allocator */ char * MyAlloc(int bytes) { char * pMem = NULL; /* Must have a size */ TRY_EXIT( bytes > 0 ); /* Allocation must succeed */ pMem = (char *)malloc(bytes); TRY_EXIT( pMem != NULL ); /* Initialize memory */ TRY_EXIT( initializeMem(pMem, bytes) != -1 ); /* Success */ return (pMem); EXIT: /* Exception: Cleanup and fail */ if (pMem != NULL) free(pMem); return (NULL); }


Esto debe pensarse en al menos dos niveles: cómo interactúan sus funciones y qué hace cuando se rompe.

La mayoría de los grandes frameworks C que veo siempre devuelven un estado y valores de "retorno" por referencia (este es el caso del WinAPI y de muchas API C Mac OS). ¿Quieres devolver un bool?

StatusCode FooBar(int a, int b, int c, bool* output);

¿Quieres devolver un puntero?

StatusCode FooBar(int a, int b, int c, char** output);

Bueno, ya captas la idea.

En el lado de la función de llamada, el patrón que veo más a menudo es usar una declaración goto que apunta a una etiqueta de limpieza:

if (statusCode < 0) goto error; /* snip */ return everythingWentWell; error: cleanupResources(); return somethingWentWrong;


Una técnica para la limpieza es usar un ciclo while que nunca se iterará. Te da goto sin usar goto.

#define NOT_ERROR(x) if ((x) < 0) break; #define NOT_NULL(x) if ((x) == NULL) break; // Initialise things that may need to be cleaned up here. char* somePtr = NULL; do { NOT_NULL(somePtr = malloc(1024)); NOT_ERROR(something(somePtr)); NOT_ERROR(somethingElse(somePtr)); // etc // if you get here everything''s ok. return somePtr; } while (0); // Something went wrong so clean-up. free(somePtr); return NULL;

Sin embargo, pierdes un nivel de sangría.

Editar: Me gustaría añadir que no tengo nada en contra de goto, es solo que para el caso de uso del interrogador realmente no lo necesita. Hay casos en que usar goto le gana a los pantalones con cualquier otro método, pero este no es uno de ellos.


La respuesta breve es: deje que sus funciones devuelvan un código de error que no puede ser un valor válido, y siempre verifique el valor de retorno. Para las funciones que devuelven punteros, esto es NULL . Para funciones que devuelven un int no negativo, es un valor negativo, comúnmente -1 , y así sucesivamente ...

Si cada posible valor de retorno también es un valor válido , use la opción de llamar por referencia:

int my_atoi(const char *str, int *val) { // convert str to int // store the result in *val // return 0 on success, -1 (or any other value except 0) otherwise }


Verificar el valor de retorno de cada función puede parecer tedioso, pero esa es la forma en que se manejan los errores en C. Considere la función nc_dial () . Todo lo que hace es verificar sus argumentos de validez y hacer una conexión de red llamando a getaddrinfo (), socket (), setsockopt (), bind () / listen () o connect (), liberando finalmente los recursos no utilizados y actualizando los metadatos. Esto podría hacerse en aproximadamente 15 líneas. Sin embargo, la función tiene casi 100 líneas debido a la comprobación de errores. Pero así es en C. Una vez que te acostumbras, puedes ocultar fácilmente la comprobación de errores en tu cabeza.

Además, no hay nada de malo con multiple if (Action() == 0) return -1; . Por el contrario: suele ser un signo de un programador prudente. Es bueno ser precavido.

Y como comentario final: no use macros para nada más que definir valores si no puede justificar su uso mientras alguien apunta con un arma en su cabeza. Más específicamente, nunca use declaraciones de flujo de control en macros: confunde al pobre individuo que tiene que mantener su código 5 años después de que dejó la compañía. No hay nada malo con if (foo) return -1; . Es simple, limpio y obvio hasta el punto de que no puedes hacer nada mejor.

Una vez que abandones tu tendencia a ocultar el flujo de control en macros, realmente no hay razón para sentir que te estás perdiendo algo.


Nunca se me ocurrió usar goto or do { } while(0) para el manejo de errores de esta manera, es bastante limpio, sin embargo, después de pensarlo, me di cuenta de que en muchos casos puedo hacer lo mismo dividiendo la función en dos:

int Foo(void) { // Initialise things that may need to be cleaned up here. char* somePtr = malloc(1024); if (somePtr = NULL) { return NULL; } if (FooInner(somePtr) < 0) { // Something went wrong so clean-up. free(somePtr); return NULL; } return somePtr; } int FooInner(char* somePtr) { if (something(somePtr) < 0) return -1; if (somethingElse(somePtr) < 0) return -1; // etc // if you get here everything''s ok. return 0; }

Esto significa ahora que obtienes una función adicional, pero de todos modos prefiero muchas funciones cortas.

Después de los consejos de Philips, también he decidido evitar el uso de macros de control de flujo, es bastante claro lo que está sucediendo, siempre y cuando los pongas en una línea.

Por lo menos, es tranquilizador saber que no solo estoy perdiendo algo, ¡todos los demás también tienen este problema! :-)


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


Probablemente no te guste escuchar esto, pero la forma C de hacer excepciones es a través de la declaración goto . Esta es una de las razones por las que está en el idioma.

La otra razón es que goto es la expresión natural de la implementación de una máquina de estado. ¿Qué tarea de programación común se representa mejor con una máquina de estado? Un analizador léxico Mire la salida de lex alguna vez. Ve a S.

Así que me parece que ahora es el momento de que te vuelvas amistoso con esa parria de elementos de sintaxis del lenguaje, el goto .