¿Se debe verificar cada pequeño error en C?


60

Como buen programador, uno debe escribir códigos robustos que manejen cada resultado de su programa. Sin embargo, casi todas las funciones de la biblioteca C devolverán 0 o -1 o NULL cuando haya un error.

A veces es obvio que se necesita una comprobación de errores, por ejemplo, cuando intenta abrir un archivo. Pero a menudo ignoro la comprobación de errores en funciones como printfo incluso mallocporque no me parece necesario.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

¿Es una buena práctica simplemente ignorar ciertos errores, o hay una mejor manera de manejar todos los errores?


13
Depende del nivel de robustez requerido para el proyecto en el que está trabajando. Los sistemas que tienen la posibilidad de recibir entradas de terceros que no son de confianza (por ejemplo, servidores públicos), o que operan en entornos no totalmente confiables, deben codificarse con mucha precaución, para evitar que el código se convierta en una bomba de tiempo (o el enlace más débil hackeado). ) Obviamente, los proyectos de hobby y aprendizaje no necesitan tanta robustez.
rwong

1
Algunos idiomas proporcionan excepciones. Si no detecta excepciones, su hilo terminará, lo cual es mejor que dejar que continúe con un mal estado. Si elige detectar excepciones, puede cubrir muchos errores en numerosas líneas de código, incluidas las funciones y métodos invocados con una sola trydeclaración, por lo que no tiene que verificar cada llamada u operación. (También tenga en cuenta que algunos idiomas son mejores que otros para detectar errores simples como la anulación de referencia o el índice de matriz fuera de los límites).
Erik Eidt

1
El problema es principalmente metodológico: normalmente no escribe la verificación de errores cuando todavía está averiguando qué se supone que debe implementar y no desea agregar la verificación de errores en este momento porque desea obtener el "camino feliz" bien primero. Pero aún se supone que debes buscar malloc y compañía. En lugar de omitir el paso de verificación de errores, debe priorizar sus actividades de codificación y dejar que la verificación de errores sea un paso de refactorización permanente implícito en su lista TODO, que se aplica cada vez que está satisfecho con su código. Agregar comentarios greppables "/ * CHECK * /" podría ayudar.
coredump

77
¡No puedo creer que nadie haya mencionado errno! En caso de que no esté familiarizado, si bien es cierto que "casi todas las funciones de la biblioteca C devolverán 0 o -1 o NULLcuando haya un error", también establecen la errnovariable global , a la que puede acceder mediante el uso #include <errno.h>y luego simplemente leyendo el valor de errno. Entonces, por ejemplo, si open(2) regresa -1, es posible que desee verificar si errno == EACCES, lo que indicaría un error de permisos, o ENOENT, lo que indicaría que el archivo solicitado no existe.
wchargin

1
@ErikEidt C no es compatible con try/ catch, aunque podría simularlo con saltos.
Mástil

Respuestas:


29

En general, el código debe tratar condiciones excepcionales donde sea apropiado. Sí, esta es una declaración vaga.

En los lenguajes de nivel superior con manejo de excepciones de software, esto a menudo se expresa como "detectar la excepción en el método en el que realmente puede hacer algo al respecto". Si se produjo un error en el archivo, tal vez deje que suba la pila al código de la interfaz de usuario que realmente puede decirle al usuario "su archivo no se pudo guardar en el disco". El mecanismo de excepción efectivamente absorbe "cada pequeño error" y lo maneja implícitamente en el lugar apropiado.

En C, no tienes ese lujo. Hay algunas formas de manejar los errores, algunas de las cuales son funciones de lenguaje / biblioteca, algunas de las cuales son prácticas de codificación.

¿Es una buena práctica simplemente ignorar ciertos errores, o hay una mejor manera de manejar todos los errores?

¿Ignorar ciertos errores? Tal vez. Por ejemplo, es razonable suponer que la escritura en la salida estándar no fallará. Si falla, ¿cómo le diría al usuario, de todos modos? Sí, es una buena idea ignorar ciertos errores o codificar a la defensiva para evitarlos. Por ejemplo, verifique cero antes de dividir.

Hay formas de manejar todos, o al menos la mayoría de los errores:

  1. Puede usar saltos, similares a los gotos, para el manejo de errores . Si bien es un tema polémico entre los profesionales del software, existen usos válidos para ellos, especialmente en el código incrustado y crítico para el rendimiento (por ejemplo, el kernel de Linux).
  1. En cascada ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. Pruebe la primera condición, por ejemplo, abrir o crear un archivo, luego suponga que las operaciones posteriores tienen éxito.

El código robusto es bueno, y uno debe verificar y manejar los errores. El método que mejor se adapte a su código depende de lo que haga el código, cuán crítico sea un fallo, etc., y solo usted puede realmente responderlo. Sin embargo, estos métodos se prueban en batalla y se usan en varios proyectos de código abierto donde puede ver cómo el código real verifica si hay errores.


21
Disculpe, un error menor para elegir aquí: "escribir en la salida estándar no fallará. Si falla, ¿cómo le diría al usuario de todos modos?" - escribiendo al error estándar? No hay garantía de que la falta de escritura en uno implique que es imposible escribir en el otro.
Damien_The_Unbeliever

44
@Damien_The_Unbeliever, especialmente porque stdoutpuede ser redirigido a un archivo, o incluso no existe dependiendo del sistema. stdoutno es una transmisión de "escritura en consola", generalmente es eso, pero no tiene que ser así.
Davor Ždralo

3
@JAB Envía un mensaje a stdout... obvio :-)
TripeHound

77
@JAB: puede salir con EX_IOERR más o menos, si eso fuera apropiado.
John Marshall

66
Hagas lo que hagas, ¡no sigas corriendo como si nada hubiera pasado! Si el comando dump_database > backups/db_dump.txtno puede escribir en la salida estándar en cualquier punto dado, no quisiera que continúe y salga con éxito. (No es que las bases de datos estén respaldadas de esa manera, pero el punto sigue en pie)
Ben S

15

Sin embargo, casi todas las funciones de la biblioteca C devolverán 0 o -1 o NULL cuando haya un error.

Sí, pero sabes a qué función llamaste, ¿no?

En realidad, tiene mucha información que podría incluir en un mensaje de error. Usted sabe qué función se llamó, el nombre de la función que la llamó, qué parámetros se pasaron y el valor de retorno de la función. Esa es mucha información para un mensaje de error muy informativo.

No tiene que hacer esto para cada llamada de función. Pero la primera vez que vea el mensaje de error "Se produjo un error al mostrar el error anterior", cuando lo que realmente necesitaba era información útil, será la última vez que vea ese mensaje de error allí, porque inmediatamente va a cambiar el mensaje de error a algo informativo que lo ayudará a solucionar el problema.


55
Esta respuesta me hizo sonreír, porque es verdad, pero no responde la pregunta.
RubberDuck

¿Cómo no responde la pregunta?
Robert Harvey

2
Una razón por la que creo que C (y otros lenguajes) mezclaron el booleano 1 y 0 es la misma razón por la que cualquier función decente que devuelve un código de error devuelve "0" sin ningún error. pero luego dices tengo if (!myfunc()) {/*proceed*/}. 0 no fue ningún error, y cualquier cosa que no sea cero fue algún tipo de error y cada tipo de error tuvo su propio código distinto de cero. La forma en que lo expresó un amigo mío fue "solo hay una Verdad, pero muchas falsedades". "0" debe ser "verdadero" en la if()prueba y todo lo que no sea cero debe ser "falso".
robert bristow-johnson

1
no, haciéndolo a tu manera, solo hay un posible código de error negativo. y muchos posibles códigos no_error.
robert bristow-johnson

1
@ robertbristow-johnson: Entonces léalo como "No hubo error". if(function()) { // do something }se lee como "si la función se ejecuta sin error, haga algo". o, mejor aún, "si la función se ejecuta con éxito, haga algo". En serio, los que entienden la convención no se confunden con esto. Devolver false(o 0) cuando ocurre un error no permitiría usar códigos de error. Cero significa falso en C porque así es como funcionan las matemáticas. Ver programmers.stackexchange.com/q/198284
Robert Harvey

11

TLDR; casi nunca debes ignorar los errores.

El lenguaje C carece de una buena característica de manejo de errores que permita a cada desarrollador de bibliotecas implementar sus propias soluciones. Los lenguajes más modernos tienen excepciones incorporadas que hacen que este problema en particular sea mucho más fácil de manejar.

Pero cuando estás atrapado con C no tienes tales ventajas. Desafortunadamente, simplemente tendrá que pagar el precio cada vez que llame a una función que existe una posibilidad remota de falla. O bien, sufrirá consecuencias mucho peores, como sobrescribir los datos en la memoria sin querer. Por lo tanto, como regla general, debe verificar los errores siempre.

Si no verifica el regreso fprintf, es muy probable que deje un error que, en el mejor de los casos, no hará lo que el usuario espera y, en el peor de los casos, explotará todo durante el vuelo. No hay excusa para debilitarte de esa manera.

Sin embargo, como desarrollador de C, también es su trabajo hacer que el código sea fácil de mantener. Por lo tanto, a veces puede ignorar los errores de manera segura en aras de la claridad si (y solo si) no representan una amenaza para el comportamiento general de la aplicación. .

Es el mismo problema que hacer esto:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Si ves que sin buenos comentarios dentro del bloque catch vacío, este es ciertamente un problema. No hay razón para pensar que una llamada malloc()se ejecutará con éxito el 100% del tiempo.


Estoy de acuerdo en que la mantenibilidad es un aspecto realmente importante y ese párrafo realmente respondió mi pregunta. Gracias por la respuesta detallada.
Derek 朕 會 功夫

Creo que los miles de millones de programas que nunca se molestan en verificar sus llamadas malloc y que nunca se bloquean es una buena razón para ser perezoso cuando un programa de escritorio solo usa unos pocos MB de memoria.
cuál es el

55
@whatsisname: el hecho de que no se bloqueen no significa que no estén corrompiendo silenciosamente los datos. malloc()¡es una función en la que definitivamente desea verificar los valores de retorno!
TMN

1
@ TMN: Si malloc fallara, el programa inmediatamente se desconectaría y terminaría, no continuaría haciendo cosas. Se trata de riesgo y de lo que es apropiado, no "casi nunca".
whatsisname

2
@Alex C no carece de un buen manejo de errores. Si desea excepciones, puede usarlas. La biblioteca estándar que no usa excepciones es una elección deliberada (las excepciones lo obligan a tener una administración de memoria automatizada y, a menudo, no desea eso para el código de bajo nivel).
martinkunev

9

La pregunta no es en realidad específica del idioma, sino específica del usuario. Piensa en la pregunta desde la perspectiva del usuario. El usuario hace algo, como escribir el nombre del programa en una línea de comando y presionar enter. ¿Qué espera el usuario? ¿Cómo pueden saber si algo salió mal? ¿Pueden permitirse el lujo de interceder si se produce un error?

En muchos tipos de código, tales controles son excesivos. Sin embargo, en el código crítico de seguridad de alta confiabilidad, como los de los reactores nucleares, la verificación de errores patológicos y las rutas de recuperación planificadas son parte de la naturaleza cotidiana del trabajo. Se considera que vale la pena tomarse el tiempo para preguntar "¿Qué sucede si X falla? ¿Cómo vuelvo a un estado seguro?" En un código menos confiable, como los de los videojuegos, puede escapar con mucha menos comprobación de errores.

Otro enfoque similar es ¿cuánto puede mejorar realmente el estado al detectar el error? No puedo contar la cantidad de programas de C ++ que orgullosamente capturan excepciones, solo para volver a lanzarlos porque en realidad no sabían qué hacer con ellos ... pero sabían que se suponía que debían manejar las excepciones. Tales programas no ganaron nada del esfuerzo extra. Solo agregue el código de verificación de errores que cree que realmente puede manejar la situación mejor que simplemente no verificar el código de error. Dicho esto, C ++ tiene reglas específicas para manejar excepciones que ocurren durante el manejo de excepciones con el fin de atraparlas de una manera más significativa (y con eso, me refiero a llamar a terminar () para asegurarse de que la pira funeraria que ha construido para usted mismo se enciende en su propia gloria)

¿Qué tan patológico puedes ser? Trabajé en un programa que definió un "SafetyBool" cuyos valores verdaderos y falsos fueron elegidos cuidadosamente para tener una distribución uniforme de 1 y 0, y fueron elegidos para que cualquiera de una serie de fallas de hardware (volteos de un solo bit, bus de datos rastros que se rompen, etc.) no causaron que un booleano se malinterpretara. No hace falta decir que no diría que se trata de una práctica de programación de propósito general para ser utilizada en cualquier programa antiguo.


6
  • Diferentes requisitos de seguridad exigen diferentes niveles de corrección. En el software de control de aviación o automóvil, todos los valores de retorno serán verificados, cf. MISRA.FUNC.UNUSEDRET . En una prueba rápida de concepto que nunca abandona su máquina, probablemente no.
  • La codificación cuesta tiempo. Demasiadas verificaciones irrelevantes en software no crítico para la seguridad es un esfuerzo que se gasta mejor en otro lugar. Pero, ¿dónde está el punto óptimo entre calidad y costo? Eso depende de las herramientas de depuración y la complejidad del software.
  • El manejo de errores puede oscurecer el flujo de control e introducir nuevos errores. Me gustan bastante las funciones de envoltura de Richard "red" Stevens que al menos informan errores.
  • El manejo de errores rara vez puede ser un problema de rendimiento. Pero la mayoría de las llamadas a la biblioteca C tardarán tanto que el costo de verificar un valor de retorno es enormemente pequeño.

3

Un poco de resumen de la cuestión. Y no es necesariamente para el lenguaje C.

Para programas más grandes, tendría una capa de abstracción; quizás una parte del motor, una biblioteca o dentro de un marco. Esa capa no se preocupan por el tiempo se obtiene un conjunto de datos válidos o la salida sería algún valor por defecto: 0, -1, nulletc.

Luego hay una capa que sería su interfaz con la capa abstracta, que manejaría muchos errores y tal vez otras cosas como inyecciones de dependencia, escucha de eventos, etc.

Y luego tendría su capa de implementación concreta donde realmente establece las reglas y maneja la salida.

Entonces, mi opinión sobre esto es que a veces es mejor excluir completamente el manejo de errores de una parte del código porque esa parte simplemente no hace ese trabajo. Y luego tenga algún procesador que evalúe la salida y señale un error.

Esto se hace principalmente para separar las responsabilidades que conducen a la legibilidad del código y a una mejor escalabilidad.


0

En general, a menos que tenga una buena razón para no verificar esa condición de error, debería hacerlo. La única buena razón por la que puedo pensar para no verificar una condición de error es cuando no es posible hacer algo significativo si falla. Sin embargo, esta es una barra muy difícil de cumplir, porque siempre hay una opción viable: salir con gracia.


En general, no estaría de acuerdo con usted, ya que los errores son situaciones que no tuvo en cuenta. Es difícil saber cómo se puede manifestar el error si no sabe en qué condiciones se originó el error. Y, por lo tanto, manejo de errores antes del procesamiento real de los datos.
Creative Magic

Como dije, si algo regresa o arroja un error, incluso si no sabe cómo solucionar el error y continuar, siempre puede fallar con gracia, registrar el error, decirle al usuario que no funcionó como se esperaba, etc. El punto es que el error no debe pasar desapercibido, no registrado, "fallar silenciosamente" o bloquear la aplicación. Eso es lo que significa "fallar con gracia" o "salir con gracia".
Neologismo inteligente
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.