lenguaje - Una forma elegante de salir de una función perfectamente sin usar goto en C
return en c (7)
¿Por qué evitar goto
?
El problema que desea resolver es: ¿Cómo asegurarse de que algún código común siempre se ejecute antes de que la función regrese a la persona que llama? Este es un problema para los programadores de C, ya que C no proporciona ningún soporte integrado para RAII.
Como ya admitiste en el cuerpo de tu pregunta, goto
es una solución perfectamente aceptable . Sin embargo, puede haber razones no técnicas para evitar su uso:
- ejercicio academico
- Codificación de conformidad estándar
- capricho personal (que creo que es lo que motiva esta pregunta)
Siempre hay más de una forma de despellejar a un gato, pero la elegancia como criterio es demasiado subjetiva como para proporcionar una manera de limitarse a una mejor alternativa. Tienes que decidir la mejor opción para ti.
Llamando explícitamente a una función de limpieza
Si se evita un salto explícito (p. Ej., goto
o break
), el código de limpieza común se puede encapsular dentro de una función, y se puede llamar explícitamente en el punto de return
temprano.
int foo () {
...
if (SOME_ERROR) {
return foo_cleanup(SOME_ERROR_CODE, ...);
}
...
}
(Esto es similar a otra respuesta publicada, que solo vi después de la publicación inicial, pero el formulario que se muestra aquí puede aprovechar las optimizaciones de llamadas entre hermanos).
Algunas personas sienten que lo explícito es más claro y, por lo tanto, más elegante. Otros sienten la necesidad de pasar argumentos de limpieza a la función para ser un detractor importante.
Añade otra capa de indirección.
Sin cambiar la semántica de la API de usuario, cambie su implementación en un contenedor compuesto por dos partes. La primera parte realiza el trabajo real de la función. La segunda parte realiza la limpieza necesaria después de que se realiza la primera parte. Si cada parte está encapsulada dentro de su propia función, la función de envoltura tiene una implementación muy limpia.
struct bar_stuff {...};
static int bar_work (struct bar_stuff *stuff) {
...
if (SOME_ERROR) return SOME_ERROR_CODE;
...
}
int bar () {
struct bar_stuff stuff = {};
int r = bar_work(&stuff);
return bar_cleanup(r, &stuff);
}
La naturaleza "implícita" de la limpieza desde el punto de vista de la función que realiza el trabajo puede ser vista favorablemente por algunos. También se evita la posibilidad de que se produzca una gran cantidad de código llamando a la función de limpieza desde un solo lugar. Algunos argumentan que los comportamientos "implícitos" son "difíciles" y, por lo tanto, más difíciles de entender y mantener.
Diverso...
Se pueden considerar soluciones más esotéricas que usan setjmp()
/ longjmp()
, pero usarlas correctamente puede ser difícil. Hay envoltorios de código abierto que implementan macros de estilo de manejo de excepción de prueba / captura sobre ellos (por ejemplo, cexcept
), pero tiene que cambiar su estilo de codificación para usar ese estilo para el manejo de errores.
También se podría considerar implementar la función como una máquina de estados. La función realiza un seguimiento del progreso a través de cada estado; un error hace que la función realice un cortocircuito al estado de limpieza. Este estilo generalmente se reserva para funciones particularmente complejas, o funciones que deben reintentarse más tarde y poder continuar desde donde se quedaron.
Haz lo que vieres.
Si necesita cumplir con los estándares de codificación, entonces el mejor enfoque es seguir la técnica más frecuente en el código base existente. Esto se aplica a casi todos los aspectos de realizar cambios en una base de código fuente estable existente. Sería considerado perjudicial introducir un nuevo estilo de codificación. Debe buscar la aprobación de los poderes existentes si cree que un cambio mejoraría drásticamente algunos aspectos del software. De lo contrario, como la "elegancia" es subjetiva, argumentar por el bien de la "elegancia" no lo llevará a ninguna parte.
A menudo escribimos algunas funciones que tienen más de un punto de salida (es decir, return
en C). Al mismo tiempo, al salir de la función, para algunos trabajos generales como la limpieza de recursos, deseamos implementarlos solo una vez, en lugar de implementarlos en cada punto de salida. Por lo general, podemos lograr nuestro deseo usando goto como el siguiente:
void f()
{
...
...{..{... if(exit_cond) goto f_exit; }..}..
...
f_exit:
some general work such as cleanup
}
Creo que usar goto aquí es aceptable , y sé que mucha gente está de acuerdo en usar goto aquí. Por curiosidad , ¿existe alguna forma elegante de salir de una función de forma ordenada sin utilizar goto en C?
Creo que la pregunta es muy interesante, pero no puede responderse sin estar influenciada por la subjetividad porque la elegancia es subjetiva. Mis ideas al respecto son las siguientes: En general, lo que desea hacer en el escenario que describe es evitar que el control pase por una serie de declaraciones a lo largo de la ruta de ejecución. Otros idiomas lo harían si se creara una excepción, que tendría que detectar.
Ya había escrito trucos para hacer lo que quieres hacer con casi todas las afirmaciones de control que hay en C, a veces en combinación, pero creo que todas son formas muy oscuras de expresar la idea de saltar a un punto especial. En lugar de eso, simplemente señalaré cómo llegamos a un punto en el que goto puede ser preferible : una vez más, lo que desea expresar utilizando es que ha ocurrido algo que impide seguir la ruta de ejecución normal. Algo que no es solo una condición regular que se puede manejar al tomar una rama diferente por el camino, sino que hace que sea imposible usar el camino hacia el punto de retorno regular de una manera segura en el estado actual . Creo que hay tres opciones para proceder en ese punto:
- volver a través de una cláusula condicional
- ir a una etiqueta de error
- Cada declaración que podría fallar está dentro de una declaración condicional, y la ejecución regular se considera una serie de operaciones condicionales.
Si su limpieza es lo suficientemente similar en todas las salidas de emergencia posibles, preferiría ir a Goto, porque escribir el código de forma redundante simplemente desordena la función. Creo que deberías intercambiar la cantidad de puntos de retorno y el código de limpieza replicado que creas contra la incomodidad de usar un goto . Ambas soluciones deben aceptarse como una elección personal del programador, a menos que existan razones graves para no hacerlo, por ejemplo, acordó que todas las funciones deben tener una sola salida. Sin embargo, el uso de cualquiera de ellas debe ser consecuente y consistente en todo el código . La tercera alternativa es - imo - el primo menos legible del goto, porque, al final, saltará a un conjunto de rutinas de limpieza, posiblemente incluidas en las declaraciones else, pero hace que sea mucho más difícil para los humanos seguir el habitual Flujo de su programa, debido a la anidación profunda de sentencias condicionales.
tl; dr: Creo que elegir entre el retorno condicional y el goto basado en las consecuentes decisiones de estilo es la forma más elegante, porque es la forma más expresiva de representar sus ideas detrás del código y la claridad es la elegancia.
He visto muchas soluciones sobre cómo hacer esto y tienden a ser oscuras, ilegibles y feas en algún grado.
Personalmente creo que la forma menos fea es esta:
int func (void)
{
if(some_error)
{
cleanup();
return result;
}
...
if(some_other_error)
{
cleanup();
return result;
}
...
cleanup();
return result;
}
Sí, utiliza dos filas de código en lugar de una. ¿Asi que? Es claro, legible, mantenible. Este es un ejemplo perfecto de dónde tienes que luchar contra tus reflejos reflejo contra la repetición del código y usar el sentido común. La función de limpieza se escribe solo una vez, todo el código de limpieza está centralizado allí.
La instrucción goto nunca es necesaria, también es más fácil escribir código sin usarlo.
En su lugar, puede utilizar una función de limpieza y volver.
if(exit_cond) {
clean_the_mess();
return;
}
O puedes romper como Vlad mencionó anteriormente. Pero un inconveniente de eso, si su bucle tiene una estructura profundamente anidada, la ruptura solo saldrá del bucle más interno. Por ejemplo:
While ( exp ) {
for (exp; exp; exp) {
for (exp; exp; exp) {
if(exit_cond) {
clean_the_mess();
break;
}
}
}
}
Solo saldrá del bucle interno interno y no abandonará el proceso.
Por ejemplo
void f()
{
do
{
...
...{..{... if(exit_cond) break; }..}..
...
} while ( 0 );
some general work such as cleanup
}
O podrías usar la siguiente estructura
while ( 1 )
{
//...
}
La principal ventaja del enfoque estructural al contrario de usar las declaraciones de goto es que introduce una disciplina en la escritura de código.
Estoy seguro y tengo la suficiente experiencia de que si una función tiene una instrucción goto, en algún momento tendrá varias declaraciones goto. :)
Soy un fan de:
void foo(exp)
{
if( ate_breakfast(exp)
&& tied_shoes(exp)
&& finished_homework(exp)
)
{
good_to_go(exp);
}
else
{
fix_the_problems(exp);
}
}
Donde ate_breakfast, tied_shoes, y completed_homework toman un puntero para exp en el que trabajan, y devuelven bools que indican un fallo de esa prueba en particular.
Es útil recordar que la evaluación de cortocircuitos funciona aquí, lo que puede calificar como un olor de código para algunas personas, pero como todos los demás han estado diciendo, la elegancia es algo subjetiva.
Supongo que elegante puede significar extraño para usted y que simplemente quiere evitar la palabra clave goto
, así que ...
Podría considerar usar setjmp(3) y longjmp
:
void foo() {
jmp_buf jb;
if (setjmp(jb) == 0) {
some_stuff();
//// etc...
if (bad_thing() {
longjmp(jb, 1);
}
};
};
No tengo idea si se ajusta a sus criterios de elegancia. (Creo que no es muy elegante, pero esto es solo una opinión ; sin embargo, no hay un goto
explícito).
Sin embargo, lo interesante es que longjmp
es un salto no local : podría haber pasado (indirectamente) jb
a some_stuff
y hacer que otra rutina (por ejemplo, llamada por some_stuff
) haga el longjmp
. Esto puede convertirse en código ilegible (así que coméntalo con prudencia).
Incluso más feo que longjmp
: use (en Linux) setcontext(3)
Lea acerca de las continuations y exceptions (y la operación call/cc en el Esquema).
Y, por supuesto, la exit(3) estándar exit(3) es una forma elegante (y útil) de salir de alguna función. A veces puedes jugar un buen truco usando también atexit(3)
Por cierto, el código del kernel de Linux se usa con frecuencia para incluirlo en algún código que se considera elegante.
Mi punto es: IMHO no seas fanático contra los goto
ya que hay casos en los que usar (con cuidado) es realmente elegante.