Forma elegante para salir de una función cuidadosamente sin usar goto en C


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 mediante el uso de 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í. Solo por curiosidad , ¿existe alguna forma elegante de salir de una función sin usar goto en C?

Author: ACcreator, 2014-09-10

7 answers

¿Por qué evitar goto?

El problema que desea resolver es: ¿Cómo asegurarse de que algún código común siempre se ejecuta antes de que la función regrese al llamante? Este es un problema para los programadores de C, ya que C no proporciona ningún soporte integrado para RAII.

Como usted ya concede en su cuerpo de la pregunta, goto es una solución perfectamente aceptable. Sin embargo, puede haber razones no técnicas para evitar el uso it:

  • ejercicio académico
  • cumplimiento de la norma de codificación
  • capricho personal (que creo que es lo que está motivando esta pregunta)

Siempre hay más de una manera de pelar un gato, pero la elegancia como criterio es demasiado subjetivo para proporcionar una manera de reducir a una sola mejor alternativa. Tienes que decidir la mejor opción para ti mismo.

Llamando explícitamente a una función de limpieza

Si se evita un salto explícito (por ejemplo, goto or break) el código de limpieza común se puede encapsular dentro de una función, y 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 publicar inicialmente, pero el formulario que se muestra aquí puede aprovechar las optimizaciones de llamadas de hermanos.)

Algunas personas sienten que la explicitud es más clara y, por lo tanto, más elegante. Otros sienten la necesidad de pasar argumentos de limpieza a la función para ser un mayor detractor.

Añade otra capa de indirección.

Sin cambiar la semántica de la API de usuario, cambie su implementación a una envoltura compuesta de dos partes. La primera parte realiza el trabajo real de la función. La segunda parte realiza la limpieza necesaria después de la primera parte. Si cada parte está encapsulada dentro de su propia función, la función wrapper 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 la función que realiza el trabajo puede ser vista favorablemente por algunos. También se evita un posible exceso de código llamando solo 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.

Varios...

Más soluciones esotéricas usando setjmp()/longjmp() se pueden considerar, pero usarlos correctamente puede ser difícil. Hay envoltorios de código abierto que implementan el manejo de excepciones try/catch estilo de macros sobre ellos (por ejemplo, cexcept), pero tienes que cambiar tu 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 rastrea el 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 volver a probarse más tarde y ser capaces de recoger desde donde se fueron fuera.

Haz lo que hacen los romanos.

Si necesita cumplir con los estándares de codificación, entonces el mejor el enfoque es seguir cualquier técnica que sea más prevalente en la base de código existente. Esto se aplica a casi todos los aspectos de hacer cambios en una base de código fuente estable existente. Se consideraría perjudicial introducir un nuevo estilo de codificación. Usted debe buscar la aprobación de los poderes fácticos si siente que un cambio mejoraría dramáticamente algún aspecto de la software. De lo contrario, como la "elegancia" es subjetiva, argumentar por el bien de la "elegancia" no te llevará a ninguna parte.

 31
Author: jxh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-11 03:01:49

Por ejemplo

void f()
{
    do
    {
         ...
         ...{..{... if(exit_cond) break; }..}..
         ...
    }  while ( 0 );

    some general work such as cleanup
}

O puede utilizar la siguiente estructura

while ( 1 )
{
   //...
}

La principal ventaja del enfoque estructural contrario al uso de sentencias goto es que introduce una disciplina en la escritura de código.

Estoy seguro y tengo suficiente experiencia de que si una función tiene una sentencia goto entonces a través de algún tiempo tendrá varias sentencias goto.:)

 6
Author: Vlad from Moscow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-10 16:42:43

He visto muchas soluciones para 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. ¿Y? Es claro, legible, mantenible. Este es un ejemplo perfecto de donde tienes que luchar contra tus reflejos instintivos contra la repetición de 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í.

 5
Author: Lundin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-10 14:06:07

Creo que la pregunta es muy interesante, pero no puede ser respondida sin ser 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 control pase a través de una serie de instrucciones a lo largo de la ruta de ejecución. Otros idiomas harían esto levantando una excepción, que tendrías que atrapar.

Ya había escrito hacks limpios para hacer lo que quieras hacer con casi todas las declaraciones 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 su lugar, simplemente explicaré cómo llegamos a un punto donde goto puede ser preferible : Una vez más, lo que desea expresar es que ha ocurrido algo que impide seguir la ruta de ejecución regular. Algo que no es solo una condición regular que se puede manejar tomando un ramificación diferente en la ruta, pero algo hace imposible usar la ruta al punto de retorno regular de una manera segura en el estado actual. Creo que hay tres opciones para proceder en ese punto:

  1. volver a través de una cláusula condicional
  2. ir a una etiqueta de error
  3. cada sentencia que podría fallar está dentro de una sentencia condicional, y la ejecución regular se considera una serie de operaciones condicionales.

Si su limpieza es similar lo suficiente en todas las salidas de emergencia posibles, preferiría el goto, porque escribir el código redundantemente solo desordena la función. Creo que deberías intercambiar el número de puntos de retorno y código de limpieza replicado que creas contra la incomodidad de usar un goto. Ambas soluciones deben ser aceptadas como una elección personal del programador, a menos que haya razones severas para no hacerlo, por ejemplo, usted acordó que todas las funciones deben tener una sola salida. Sin embargo, el uso de cualquiera de los dos debe ser consecuente y consistente a través del código. La tercera alternativa es-imo - el primo menos legible del goto, porque, al final se saltará a un conjunto de rutinas de limpieza - posiblemente encerrados por else-declaraciones también, pero hace que sea mucho más difícil para los seres humanos seguir el flujo regular de su programa, debido a la anidación profunda de las declaraciones condicionales.

Tl; dr: Creo que elegir entre retorno condicional y goto basado en decisiones de estilo consecuentes es lo más elegante manera, porque es la forma más expresiva de representar sus ideas detrás del código y la claridad es la elegancia.

 5
Author: midor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-11 12:14:42

Supongo que elegante puede significar para ti raro y que simplemente quieres 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 una goto explícita).

Sin embargo, lo interesante es que longjmp es un salto no local : Podrías haber pasado (indirectamente) jb a some_stuff y tener alguna otra rutina (por ejemplo, llamada por some_stuff) hacer el longjmp. Esto puede convertirse en código ilegible(así que coméntalo sabiamente).

Aún más feo que longjmp: use (en Linux) setcontext(3)

Lea acerca de continuacionesy excepciones (y la operación call/cc en Scheme).

Y por supuesto, el estándar exit(3) es una forma elegante (y útil) de salir de alguna función. A veces se puede jugar aseado truco también usando atexit(3)

POR cierto, el código del kernel de Linux utiliza bastante a menudo goto incluyendo en algún código que se considera elegante.

Mi punto es : IMHO no seas fanático contra goto-s ya que hay casos en los que usar (con cuidado) es de hecho elegante.

 4
Author: Basile Starynkevitch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-10 14:23:18

Soy 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 finished_homework toman un puntero a exp en el que trabajan, y devuelven bools indicando un fallo de esa prueba en particular.

Ayuda recordar que la evaluación de cortocircuitos está trabajando aquí, lo que puede calificar como un olor a código para algunas personas, pero como todos los demás han estado diciendo, la elegancia es algo subjetiva.

 3
Author: int Pi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-10 20:59:14

La instrucción Goto nunca es necesaria, también es más fácil escribir código sin usarlo.

En su lugar, puede usar una función más limpia y regresar.

if(exit_cond) {
    clean_the_mess();
    return;
}

O puede romper como Vlad mencionado anteriormente. Pero un inconveniente de eso, si su bucle tiene una ruptura de estructura profundamente anidada 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 for interno y no abandonará el proceso.

 -1
Author: Kürşat Kobya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-10 14:27:20