2012-10-09 21 views
6

Tengo una pregunta sobre el uso de la sentencia goto en C++. Entiendo que este tema es controvertido y no me interesan los consejos o argumentos radicales (generalmente me alejo del uso de goto). Más bien, tengo una situación específica y quiero entender si mi solución, que hace uso de la declaración goto, es buena o no. No me llamaría nuevo en C++, pero tampoco me clasificaría como programador de nivel profesional. La parte del código que generó mi pregunta gira en un bucle infinito una vez que comenzó. El flujo general de la rosca en pseudocódigo es el siguiente:Uso de goto para salir limpiamente de un bucle

void ControlLoop::main_loop() 
{ 
    InitializeAndCheckHardware(pHardware) //pHardware is a pointer given from outside 
    //The main loop 
    while (m_bIsRunning) 
    { 
     simulated_time += time_increment; //this will probably be += 0.001 seconds 
     ReadSensorData(); 
     if (data_is_bad) { 
      m_bIsRunning = false; 
      goto loop_end; 
     }  
     ApplyFilterToData(); 
     ComputeControllerOutput(); 
     SendOutputToHardware(); 
     ProcessPendingEvents(); 

     while (GetWallClockTime() < simulated_time) {} 
     if (end_condition_is_satisified) m_bIsRunning = false; 
    } 
    loop_end: 
    DeInitializeHardware(pHardware); 
} 

El puntero se pasa en pHardware desde fuera del objeto ControlLoop y tiene un tipo polimórfico, por lo que no tiene mucho sentido para mí hacer uso de RAII y para crear y destruir la interfaz de hardware dentro de main_loop. Supongo que podría hacer que pHardware cree un objeto temporal que represente una especie de "sesión" o "uso" del hardware que podría limpiarse automáticamente al salir de main_loop, pero no estoy seguro de si esa idea lo haría más claro para alguien de lo contrario, cuál es mi intención. Solo habrá tres formas de salir del ciclo: la primera es si se leen datos incorrectos del hardware externo; el segundo es si ProcessPendingEvents() indica un aborto iniciado por el usuario, que simplemente hace que m_bIsRunning se vuelva falso; y el último es si la condición final se cumple en la parte inferior del ciclo. También debería señalar que main_loop podría iniciarse y finalizarse varias veces durante la vida del objeto ControlLoop, por lo que debería salir limpiamente con m_bIsRunning = false después.

Además, me di cuenta de que podía utilizar la palabra reservada break, aunque la mayoría de estas funciones pseudocódigo llamadas dentro main_loop realmente no están encapsulados como funciones, simplemente porque tendrían que o bien tienen muchos argumentos o todos ellos necesitarían acceso a variables miembro Ambos casos serían más confusos, en mi opinión, que simplemente dejar main_loop como una función más larga, y debido a la longitud del ciclo big while, una afirmación como goto loop_end parece ser más clara para mí.

Ahora para la pregunta: ¿Esta solución lo haría sentir incómodo si lo escribiera en su propio código? Me parece un poco erróneo, pero nunca antes utilicé la sentencia goto en el código C++, de ahí mi solicitud de ayuda de expertos. ¿Hay alguna otra idea básica que me falta que haga que este código sea más claro?

Gracias.

+4

"no tiene mucho sentido para mí hacer uso de RAII" Siempre tiene sentido usar RAII. Siempre. Todo el tiempo. –

+10

¿Por qué no utilizar 'break' aquí? –

+3

Si solo hay un bucle que está rompiendo, use 'break'; es más limpio y más claro. Si tiene múltiples niveles de bucle, o si está dentro de un 'interruptor', y necesita salir de todos los bucles, entonces el 'goto' está bien. –

Respuesta

3

Con su sola condición, singular, que hace que el bucle de romper temprana Me basta con utilizar una break. No es necesario un goto para el cual es break.

Sin embargo, si cualquier de esas llamadas de función puede lanzar una excepción o si termina encima de necesitar múltiples break s yo preferiría un contenedor estilo RAII, esto es exactamente el tipo de cosas destructores son para. Siempre se realiza la llamada a DeInitializeHardware, así que ...

// todo: add error checking if needed 
class HardwareWrapper { 
public: 
    HardwareWrapper(Hardware *pH) 
     : _pHardware(pH) { 
     InitializeAndCheckHardware(_pHardware); 
    } 

    ~HardwareWrapper() { 
     DeInitializeHardware(_pHardware); 
    } 

    const Hardware *getHardware() const { 
     return _pHardware; 
    } 

    const Hardware *operator->() const { 
     return _pHardware; 
    } 

    const Hardware& operator*() const { 
     return *_pHardware; 
    } 

private: 
    Hardware *_pHardware; 
    // if you don't want to allow copies... 
    HardwareWrapper(const HardwareWrapper &other); 
    HardwareWrapper& operator=(const HardwareWrapper &other); 
} 

// ... 

void ControlLoop::main_loop() 
{ 
    HardwareWrapper hw(pHardware); 
    // code 
} 

Ahora, no importa lo que pase, siempre va a llamar DeInitializeHardware cuando esa función regrese.

+0

Esta solución es muy limpia. Gracias. – Hunter

+0

Dado que hay una función 'InitializeXXX', sugeriría poner la inicialización en el constructor, para la simetría. –

5

Evitar el uso de goto es una cosa bastante sólida que hacer en el desarrollo orientado a objetos en general.

En su caso, ¿por qué no utilizar break para salir del ciclo?

while (true) 
{ 
    if (condition_is_met) 
    { 
     // cleanup 
     break; 
    } 
} 

cuanto a su pregunta: el uso de goto me haría incómodo. La única razón por la cual break es menos legible es su admisión a no ser un desarrollador fuerte de C++. Para cualquier desarrollador avezado de un lenguaje similar a C, break leerá mejor, además de proporcionar una solución más limpia que goto.

En particular, simplemente no estoy de acuerdo que

if (something) 
{ 
    goto loop_end; 
} 

es más legible que

if (something) 
{ 
    break; 
} 

que literalmente dice lo mismo con la sintaxis incorporado.

+0

Mi principal preocupación era que el ciclo principal en el código real es bastante largo, así que pensé que el uso del break hace que sea un poco menos claro dónde el flujo del programa irá inmediatamente leyéndolo. Gracias, sin embargo, por tu comentario. Esto parece un punto de vista razonable. – Hunter

+0

Siempre y cuando no tenga un espaciado impar en su código, su código debería fluir de forma bastante legible utilizando los paréntesis 'while {}' para cualquier desarrollador experimentado. Es particularmente fácil seguir lo que está fuera del ciclo. – pickypg

+0

Además, es posible que un IDE le diga exactamente dónde irá el 'break' (aunque no sé si hay IDEs que tengan esa característica en particular). –

1

En lugar de utilizar goto, se debe utilizar break; escapar bucles.

+1

Para un solo nivel de bucle, eso es cierto. ¿Qué hay de los múltiples niveles de loop? –

+1

El póster estaba usando goto para salir de ese único bucle, por lo tanto, break es la mejor opción (IMO) en el caso del OP. – zachjs

+1

De acuerdo; usted ha respondido la letra de la pregunta. Pero las respuestas que van más allá de la letra de la pregunta y responden al espíritu de la pregunta tienen más probabilidades de ser votadas. El problema no es que 'goto' sea automáticamente malo; no es correcto en el ejemplo, pero no es difícil idear circunstancias en las que sea apropiado. –

3

ACTUALIZACIÓN

Si su principal preocupación es el bucle while es demasiado largo, entonces usted debería apuntar a que sea más corto, C++ es un lenguaje orientado a objetos y OO es para dividir las cosas en pedazos pequeños y componentes, incluso en general, el idioma no es OO, en general, creemos que debemos dividir un método/bucle en uno pequeño y facilitar la lectura. Si un ciclo tiene 300 líneas, sin importar break/goto realmente no le ahorra tiempo, ¿no es así?

ACTUALIZACIÓN

No estoy en contra Goto, pero no voy a utilizar aquí como lo hace, prefiero sólo tiene que utilizar ruptura, en general, a un desarrollador que vio un descanso allí él sabe que significa ir al final del tiempo, y con eso m_bIsRunning = false puede fácilmente darse cuenta de que en realidad está saliendo del bucle en cuestión de segundos. Sí, un goto puede ahorrarle tiempo para entenderlo pero también puede hacer que la gente se sienta nerviosa con respecto a su código.

Lo que puedo imaginar que estoy usando un Goto sería para salir de un bucle de dos niveles:

while(running) 
{ 
    ... 
    while(runnning2) 
    { 
     if(bad_data) 
     { 
      goto loop_end; 
     } 
    } 
    ... 
} 
loop_end: 
+0

Gracias, Simon. El ciclo principal no es ridículamente largo (~ 150 líneas), pero es más largo que una pantalla, al menos para mí. Ciertamente podría dividirlo más. Sin embargo, ya he hecho esto en la medida que creí razonable. Esto puede pertenecer a otra pregunta y, si es así, no dude en no responder, pero podría hacer que las subrutinas clasifiquen métodos que se comunican mediante el almacenamiento de datos en la clase, o simplemente funciones en la misma unidad de traducción que toman argumentos para el resultado. – Hunter

+0

Bueno, eso realmente depende, y no sé qué haría hasta que lo logre;) La regla que seguiría principalmente sería hacer que cada clase se concentre en un área con solo hacer una cosa, sin embargo, nadie es perfecto y yo podría hacer una codificación fea de la que no puedo darme cuenta. Yo diría que deje el código tal como está allí si usted y su equipo se sienten bien con ese goto y nadie quiere eliminarlo. –

0

Goto no es una buena práctica para salir de bucle cuando break es una opción.

Además, en rutinas complejas, es bueno tener solo una lógica de salida (con limpieza) colocada al final. Goto a veces se usa para saltar a la lógica de retorno.

Ejemplo de QEMU controlador de bloque vmdk:

static int vmdk_open(BlockDriverState *bs, int flags) 
{ 
    int ret; 
    BDRVVmdkState *s = bs->opaque; 

    if (vmdk_open_sparse(bs, bs->file, flags) == 0) { 
     s->desc_offset = 0x200; 
    } else { 
     ret = vmdk_open_desc_file(bs, flags, 0); 
     if (ret) { 
      goto fail; 
     } 
    } 
    /* try to open parent images, if exist */ 
    ret = vmdk_parent_open(bs); 
    if (ret) { 
     goto fail; 
    } 
    s->parent_cid = vmdk_read_cid(bs, 1); 
    qemu_co_mutex_init(&s->lock); 

    /* Disable migration when VMDK images are used */ 
    error_set(&s->migration_blocker, 
       QERR_BLOCK_FORMAT_FEATURE_NOT_SUPPORTED, 
       "vmdk", bs->device_name, "live migration"); 
    migrate_add_blocker(s->migration_blocker); 

    return 0; 

fail: 
    vmdk_free_extents(bs); 
    return ret; 
} 
1

Hay varias alternativa a goto: break, continue y return dependiendo de la situación.

Sin embargo, debe tener en cuenta que tanto break como continue están limitados, ya que solo afectan al bucle más interno. return por otro lado no se ve afectado por esta limitación.

En general, si se utiliza un goto-salida un ámbito determinado, entonces se puede refactorizar utilizando otra función y una declaración return lugar. Es probable que hará que el código sea más fácil de leer como un bono:

// Original 
void foo() { 
    DoSetup(); 
    while (...) { 
     for (;;) { 
      if() { 
       goto X; 
      } 
     } 
    } 
    label X: DoTearDown(); 
} 

// Refactored 
void foo_in() { 
    while (...) { 
     for (;;) { 
      if() { 
       return; 
      } 
     } 
    } 
} 

void foo() { 
    DoSetup(); 
    foo_in(); 
    DoTearDown(); 
} 

Nota: si su cuerpo de la función no puede caber cómodamente en su pantalla, lo está haciendo mal.

+0

Gracias, esta es otra solución que definitivamente vale la pena considerar. – Hunter

0

Estoy viendo mucha gente que sugiere break en lugar de goto. Pero break no es "mejor" (o "peor") que goto.

La inquisición contra goto eficazmente comenzó con Dijkstra "Go To Considered Harmful" paper en 1968, cuando el código spaghetti era la regla y cosas como bloques estructurados if y while declaraciones seguían siendo considerados de vanguardia. ALGOL 60 los tenía, pero era esencialmente un lenguaje de investigación utilizado por académicos (ver ML hoy); ¡Fortran, uno de los idiomas dominantes en ese momento, no los obtendría por otros 9 años!

Los principales puntos en el papel de Dijkstra son:

  1. Los seres humanos son buenos en el razonamiento espacial y programas de bloques estructurados sacar provecho de eso porque las acciones del programa que se producen cerca unos de otros en el tiempo se describen cerca unos de otros en " espacio "(código de programa);
  2. Si evita goto en todas sus formas, entonces es posible saber cosas sobre los posibles estados de las variables en cada posición léxica en el programa. En particular, al final de un bucle while, usted sabe que la condición de ese bucle debe ser falsa. Esto es útil para la depuración. (Dijkstra no acaba de decir esto, pero se puede inferir que.)

break, al igual que goto (y principios de return s, y las excepciones ...), reduce (1) y elimina (2). Por supuesto, usar break a menudo le permite evitar escribir la lógica intrincada para la condición while, obteniendo una ganancia neta en comprensibilidad, y exactamente lo mismo aplica para goto.

+0

Gracias por la explicación. También es bueno darse cuenta de que sin un 'break' o' goto' en mi caso específico, el resto del ciclo while debería estar protegido por un 'if' adicional, ya que enviar resultados al hardware basado en una entrada incorrecta podría ser desastroso. , es decir, la respuesta correcta a datos incorrectos es "detener ahora". – Hunter

Cuestiones relacionadas