Esta pregunta es en realidad el resultado de una interesante discusión en programming.reddit.com hace un tiempo. Básicamente se reduce al siguiente código:
int foo(int bar)
{
int return_value = 0;
if (!do_something( bar )) {
goto error_1;
}
if (!init_stuff( bar )) {
goto error_2;
}
if (!prepare_stuff( bar )) {
goto error_3;
}
return_value = do_the_thing( bar );
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
El uso de goto
aquí parece ser la mejor manera de hacerlo, lo que resulta en el código más limpio y eficiente de todas las posibilidades, o al menos eso me parece a mí. Citando a Steve McConnell en Código completo :
Goto es útil en una rutina que asigna recursos, realiza operaciones en esos recursos y luego desasigna los recursos. Con un goto, puede limpiar en una sección del código. Ir a reduce la probabilidad de que se olvide de desasignar los recursos en cada lugar en el que detecte un error.
Otro soporte para este enfoque proviene del libro Controladores de dispositivos de Linux , en esta sección .
¿Qué piensas? ¿Es este caso un uso válido para goto
en C? ¿Preferiría otros métodos que produzcan un código más complicado y / o menos eficiente, pero evite goto
?
fuente
on error goto error
sistema de manejo de errores :)Respuestas:
FWIF, encuentro que el lenguaje de manejo de errores que dio en el ejemplo de la pregunta es más legible y más fácil de entender que cualquiera de las alternativas dadas en las respuestas hasta ahora. Si bien
goto
es una mala idea en general, puede ser útil para el manejo de errores cuando se hace de una manera simple y uniforme. En esta situación, aunque sea ungoto
, se está utilizando de forma bien definida y más o menos estructurada.fuente
Como regla general, evitar el goto es una buena idea, pero los abusos que prevalecían cuando Dijkstra escribió por primera vez 'GOTO Considered Dañino' ni siquiera cruzan la mente de la mayoría de las personas como una opción en estos días.
Lo que usted describe es una solución generalizable para el problema de manejo de errores; para mí está bien siempre que se use con cuidado.
Su ejemplo particular se puede simplificar de la siguiente manera (paso 1):
Continuando el proceso:
Esto es, creo, equivalente al código original. Esto parece particularmente limpio ya que el código original en sí mismo estaba muy limpio y bien organizado. A menudo, los fragmentos de código no son tan ordenados como eso (aunque aceptaría un argumento de que deberían serlo); por ejemplo, con frecuencia hay más estados para pasar a las rutinas de inicialización (configuración) que los que se muestran y, por lo tanto, también hay más estados para pasar a las rutinas de limpieza.
fuente
Me sorprende que nadie haya sugerido esta alternativa, así que aunque la pregunta ha existido por un tiempo, la agregaré: una buena manera de abordar este problema es usar variables para realizar un seguimiento del estado actual. Esta es una técnica que se puede utilizar tanto si se utiliza como si no
goto
para llegar al código de limpieza. Como cualquier técnica de codificación, tiene pros y contras, y no será adecuada para cada situación, pero si elige un estilo, vale la pena considerarlo, especialmente si desea evitarlogoto
sin terminar conif
s profundamente anidados .La idea básica es que, por cada acción de limpieza que deba realizarse, existe una variable a partir de cuyo valor podemos saber si la limpieza debe realizarse o no.
goto
Primero mostraré la versión, porque está más cerca del código de la pregunta original.Una ventaja de esto sobre algunas de las otras técnicas es que, si se cambia el orden de las funciones de inicialización, la limpieza correcta aún ocurrirá, por ejemplo, usando el
switch
método descrito en otra respuesta, si el orden de inicialización cambia, entonces elswitch
tiene que editarse con mucho cuidado para evitar intentar limpiar algo que no se inicializó realmente en primer lugar.Ahora, algunos podrían argumentar que este método agrega una gran cantidad de variables adicionales, y de hecho en este caso eso es cierto, pero en la práctica, a menudo, una variable existente ya rastrea, o se puede hacer que rastree, el estado requerido. Por ejemplo, si en
prepare_stuff()
realidad es una llamada amalloc()
, o aopen()
, entonces se puede usar la variable que contiene el puntero devuelto o el descriptor de archivo, por ejemplo:Ahora, si adicionalmente hacemos un seguimiento del estado de error con una variable, podemos evitarlo por
goto
completo y aún así limpiar correctamente, sin tener una sangría que se vuelve más y más profunda cuanto más inicialización necesitamos:Nuevamente, hay posibles críticas a esto:
if (oksofar)
comprobaciones fallidas para un solo salto al código de limpieza (GCC ciertamente lo hace) y, en cualquier caso, el caso de error suele ser menos crítico para el rendimiento.¿No es esto agregar otra variable más? En este caso sí, pero a menudo la
return_value
variable se puede utilizar para desempeñar el papel queoksofar
está jugando aquí. Si estructura sus funciones para devolver errores de manera consistente, incluso puede evitar el segundoif
en cada caso:Una de las ventajas de codificar así es que la consistencia significa que cualquier lugar donde el programador original se haya olvidado de verificar el valor de retorno sobresale como un pulgar dolorido, lo que hace que sea mucho más fácil encontrar (esa clase de) errores.
Entonces, este es (todavía) un estilo más que se puede usar para resolver este problema. Si se usa correctamente, permite un código muy limpio y consistente, y como cualquier técnica, en las manos equivocadas puede terminar produciendo un código largo y confuso :-)
fuente
El problema con la
goto
palabra clave se malinterpreta en su mayoría. No es pura maldad. Solo necesita ser consciente de las rutas de control adicionales que crea con cada goto. Se vuelve difícil razonar sobre su código y, por lo tanto, su validez.FWIW, si busca los tutoriales de developer.apple.com, adoptan el enfoque goto para el manejo de errores.
No usamos gotos. Se concede mayor importancia a los valores de retorno. El manejo de excepciones se realiza a través de
setjmp/longjmp
lo poco que pueda.fuente
No hay nada moralmente incorrecto en la declaración goto más de lo que hay algo moralmente incorrecto con (nulo) * punteros.
Todo depende de cómo utilice la herramienta. En el caso (trivial) que presentó, una declaración de caso puede lograr la misma lógica, aunque con más gastos generales. La verdadera pregunta es, "¿cuál es mi requisito de velocidad?"
goto es simplemente rápido, especialmente si tiene cuidado de asegurarse de que se compile en un salto corto. Perfecto para aplicaciones donde la velocidad es una prima. Para otras aplicaciones, probablemente tenga sentido tomar el golpe de sobrecarga con if / else + case para el mantenimiento.
Recuerde: goto no mata aplicaciones, los desarrolladores matan aplicaciones.
ACTUALIZACIÓN: Aquí está el ejemplo de caso
fuente
GOTO es útil. Es algo que su procesador puede hacer y es por eso que debería tener acceso a él.
A veces, desea agregar algo a su función y solo ir a le permite hacerlo fácilmente. Puede ahorrar tiempo ...
fuente
En general, consideraría el hecho de que un fragmento de código podría escribirse con mayor claridad utilizando
goto
como síntoma que el flujo del programa es probablemente más complicado de lo que es generalmente deseable. La combinación de otras estructuras de programas de formas extrañas para evitar el usogoto
intentaría tratar el síntoma, en lugar de la enfermedad. Es posible que su ejemplo particular no sea demasiado difícil de implementar singoto
:pero si se suponía que la limpieza solo iba a ocurrir cuando la función fallaba, el
goto
caso podría manejarse poniendo unreturn
justo antes de la primera etiqueta de destino. El código anterior requeriría agregar unreturn
en la línea marcada con*****
.En el escenario de "limpieza incluso en caso normal", consideraría el uso de
goto
como más claro que las construccionesdo
/while(0)
, entre otras cosas porque las etiquetas de destino en sí mismas prácticamente gritan "MÍRAME" mucho más que las construccionesbreak
ydo
/while(0)
. Para el caso de "limpieza solo si hay error", lareturn
declaración termina teniendo que estar en el peor lugar posible desde el punto de vista de la legibilidad (las declaraciones de retorno generalmente deben estar al comienzo de una función o en lo que "parece" el fin); tener unareturn
etiqueta justo antes de un objetivo cumple con esa calificación mucho más fácilmente que tener una justo antes del final de un "ciclo".Por cierto, un escenario donde a veces uso
goto
para el manejo de errores es dentro de unaswitch
declaración, cuando el código para varios casos comparte el mismo código de error. Aunque mi compilador a menudo sería lo suficientemente inteligente como para reconocer que varios casos terminan con el mismo código, creo que es más claro decir:Aunque se podrían reemplazar las
goto
declaraciones con{handle_error(); break;}
, y aunque se podría usar undo
/while(0)
loop junto concontinue
para procesar el paquete de ejecución condicional envuelto, realmente no creo que sea más claro que usar ungoto
. Además, si bien es posible copiar el código dePACKET_ERROR
todos los lugares dondegoto PACKET_ERROR
se usa, y mientras que un compilador puede escribir el código duplicado una vez y reemplazar la mayoría de las ocurrencias con un salto a esa copia compartida, el uso degoto
hace que sea más fácil notar los lugares que configura el paquete de forma un poco diferente (por ejemplo, si la instrucción "ejecutar condicionalmente" decide no ejecutar).fuente
Personalmente, soy un seguidor de "El poder de diez - Diez reglas para escribir un código crítico de seguridad" .
Incluiré un pequeño fragmento de ese texto que ilustra lo que creo que es una buena idea sobre goto.
Regla: Restrinja todo el código a construcciones de flujo de control muy simples; no utilice sentencias goto, construcciones setjmp o longjmp y recursividad directa o indirecta.
Justificación: Un flujo de control más simple se traduce en capacidades más sólidas para la verificación y, a menudo, resulta en una mayor claridad del código. El destierro de la recursividad es quizás la mayor sorpresa aquí. Sin embargo, sin recursividad, tenemos la garantía de tener un gráfico de llamada de función acíclica, que puede ser explotado por analizadores de código y puede ayudar directamente a demostrar que todas las ejecuciones que deberían estar limitadas son de hecho limitadas. (Tenga en cuenta que esta regla no requiere que todas las funciones tengan un único punto de retorno, aunque esto a menudo también simplifica el flujo de control. Sin embargo, hay suficientes casos en los que un retorno de error temprano es la solución más simple).
Desterrar el uso de goto parece malo pero:
Si las reglas parecen draconianas al principio, tenga en cuenta que están destinadas a hacer posible verificar el código donde, literalmente, su vida puede depender de su corrección: código que se usa para controlar el avión en el que vuela, la planta de energía nuclear a pocas millas de donde vives, o la nave espacial que lleva a los astronautas a la órbita. Las reglas actúan como el cinturón de seguridad en su automóvil: inicialmente son quizás un poco incómodas, pero después de un tiempo su uso se vuelve una segunda naturaleza y no usarlas se vuelve inimaginable.
fuente
goto
es usar algún conjunto de booleanos "inteligentes" en ifs o bucles profundamente anidados. Eso realmente no ayuda. Tal vez tus herramientas lo asimilen mejor, pero tú no lo harás y tú eres más importante.Estoy de acuerdo en que la limpieza goto en orden inverso dado en la pregunta es la forma más limpia de limpiar las cosas en la mayoría de las funciones. Pero también quería señalar que, a veces, desea que su función se limpie de todos modos. En estos casos utilizo la siguiente variante if if (0) {label:} modismo para ir al punto correcto del proceso de limpieza:
fuente
Me parece que
cleanup_3
debería hacer su limpieza, luego llamarcleanup_2
. Del mismo modo,cleanup_2
debería hacer su limpieza, luego llamar a cleanup_1. Parece que siempre que lo hagacleanup_[n]
,cleanup_[n-1]
es necesario, por lo que debería ser responsabilidad del método (de modo que, por ejemplo,cleanup_3
nunca se pueda llamar sin llamarcleanup_2
y posiblemente causar una fuga).Dado ese enfoque, en lugar de gotos, simplemente llamaría a la rutina de limpieza y luego regresaría.
Sin embargo, el
goto
enfoque no es incorrecto ni está mal , solo vale la pena señalar que no es necesariamente el enfoque "más limpio" (en mi humilde opinión).Si está buscando el rendimiento óptimo, supongo que la
goto
solución es la mejor. Sin embargo, solo espero que sea relevante en unas pocas aplicaciones seleccionadas, críticas para el rendimiento (por ejemplo, controladores de dispositivos, dispositivos integrados, etc.). De lo contrario, es una microoptimización que tiene menor prioridad que la claridad del código.fuente
Creo que la pregunta aquí es falaz con respecto al código dado.
Considerar:
Por lo tanto: do_something (), init_stuff () y prepare_stuff () deberían estar haciendo su propia limpieza . Tener una función cleanup_1 () separada que limpia después de do_something () rompe la filosofía de encapsulación. Es un mal diseño.
Si hicieron su propia limpieza, entonces foo () se vuelve bastante simple.
Por otra parte. Si foo () realmente creara su propio estado que necesitaba ser derribado, entonces goto sería apropiado.
fuente
Esto es lo que he preferido:
fuente
Sin embargo, una vieja discusión ... ¿qué pasa con el uso de "anti patrón de flecha" y encapsular más tarde cada nivel anidado en una función estática en línea? El código se ve limpio, es óptimo (cuando las optimizaciones están habilitadas) y no se usa goto. En resumen, divide y vencerás. A continuación un ejemplo:
En términos de espacio, estamos creando tres veces la variable en la pila, lo cual no es bueno, pero esto desaparece al compilar con -O2 eliminando la variable de la pila y usando un registro en este simple ejemplo. Lo que obtuve del bloque anterior
gcc -S -O2 test.c
fue el siguiente:fuente
Prefiero usar la técnica descrita en el siguiente ejemplo ...
}
fuente: http://blog.staila.com/?p=114
fuente
Usamos la
Daynix CSteps
biblioteca como otra solución para el " problema goto " en las funciones de inicio.Vea aquí y aquí .
fuente
goto