¿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 ...
fuente
Respuestas:
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.
fuente
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?
fuente
assert(X)
donde X es cualquier declaración C válida que desea que sea verdad. ver stackoverflow.com/q/1571340/10396 .assert(X!=NULL);
oassert(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.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:
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:
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.
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
new
hace bien al proporcionar una variante que no arroja.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.
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.
fuente
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.
Permite un manejo simple y estandarizado de errores.
Permite el manejo simple de errores en la función de biblioteca.
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.
fuente
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
fuente
ETRY
código ha sido revisado desde que se escribió esta respuesta.setjmp
es 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 basasetjmp
y hace que su código sea hasta un 30% más lento en la práctica.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.fuente
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.
fuente
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
err
y debe ser el último argumento. Entonces, el script puede coincidir con la cadena yerr);
luego verificar si es seguida porif (*err
. En realidad, en la práctica creamos una macro llamadaCER
(check err return) yCEG
(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.
fuente
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:
fuente
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
errno
un 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í:que está entre tus dos opciones :-).
fuente
También estuve reflexionando sobre este problema recientemente, y escribí algunas macros para C que simulan la semántica try-catch-finally usando valores de retorno puramente locales. Esperamos que te sea útil.
fuente
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:
fuente
El primer enfoque es mejor en mi humilde opinión:
fuente
Definitivamente prefiero la primera solución:
lo modificaría ligeramente para:
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 menosundo
que se pueda llamar a alguna función para manejar el manejo de errores correctamente.fuente
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:
Luego, en la función llamada:
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 (
returnedSize
oreturnedError
en los dos métodos que presentó).fuente
Aquí hay un programa simple para demostrar las 2 primeras viñetas de la respuesta de Nils Pipenbrinck aquí .
Sus primeras 2 balas son:
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
mymodule.c contiene mi función de mapeo para mapear desde códigos de error de enumeración a cadenas C imprimibles:
mymodule.c
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
Salida:
Referencias
Puede ejecutar este código usted mismo aquí: https://onlinegdb.com/ByEbKLupS .
fuente
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
Una alternativa es tener un error siempre devuelto como cero y usar una función LastError () para proporcionar detalles del error real.
fuente
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
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:
Un beneficio de los valores de retorno es que permite el encadenamiento de llamadas para un manejo de errores menos intrusivo:
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):
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
Un indicador de error solo le dice que hay un problema pero nada sobre la naturaleza de dicho problema:
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:
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:
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.
fuente
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:
fuente
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
fuente
Prefiero el manejo de errores en C usando la siguiente técnica:
Fuente: http://blog.staila.com/?p=114
fuente
goto
's en lugar de repetidosif
'. Referencias: una , dos .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:
Cuando tiene muchas comprobaciones de errores, esta pequeña simplificación realmente ayuda.
fuente