¿Cuáles son algunas formas mejores de evitar el do-while (0); piratear en C ++?


233

Cuando el flujo de código es así:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

En general, he visto que esto funciona para evitar el flujo de código desordenado anterior:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

¿Cuáles son algunas formas mejores de evitar esta solución / pirateo para que se convierta en un código de nivel superior (nivel industrial)?

¡Cualquier sugerencia que esté lista para usar es bienvenida!


38
RAII y lanzar excepciones.
ta.speot.is

135
Para mí, esto parece un buen momento para usar goto, pero estoy seguro de que alguien me rebajará por sugerir eso, así que no estoy escribiendo una respuesta a ese efecto. Tener un LARGO do ... while(0);parece ser lo incorrecto.
Mats Petersson

42
@dasblinkenlight: Sí, de hecho. Si va a usar goto, sea honesto y hágalo a la intemperie, no lo oculte usando breakydo ... while
Mats Petersson

44
@MatsPetersson: Responda, le daré +1 por su gotoesfuerzo de rehabilitación. :)
wilx

27
@ ta.speot.is: "RAII y lanzar excepciones". Al hacerlo, estará emulando el control de flujo con excepciones. Es decir, es como usar hardware costoso y de última generación como martillo o pisapapeles. Puedes hacer eso, pero eso definitivamente me parece un mal gusto.
SigTerm

Respuestas:


310

Se considera una práctica aceptable aislar estas decisiones en una función y usar returns en lugar de breaks. Si bien todas estas verificaciones corresponden al mismo nivel de abstracción que la función, es un enfoque bastante lógico.

Por ejemplo:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}

22
@MatsPetersson: "Aislar en función" significa refactorizar en una nueva función que solo realiza las pruebas.
MSalters

35
+1. Esta también es una buena respuesta. En C ++ 11, la función aislada puede ser una lambda, ya que también puede capturar las variables locales, ¡así que facilita las cosas!
Nawaz

44
@deworde: Dependiendo de la situación, esta solución puede ser mucho más larga y menos legible que goto. Debido a que C ++, desafortunadamente, no permite definiciones de funciones locales, tendrá que mover esa función (de la que va a regresar) a otro lugar, lo que reduce la legibilidad. Podría terminar con docenas de parámetros, entonces, debido a que tener docenas de parámetros es algo malo, decidirá envolverlos en struct, lo que creará un nuevo tipo de datos. Demasiado escribiendo para una situación simple.
SigTerm

24
@Damon, en todo caso, returnestá más limpio porque cualquier lector es inmediatamente consciente de que funciona correctamente y de lo que hace. Con gotousted debe mirar a su alrededor para ver para qué sirve y para asegurarse de que no se cometieron errores. El disfraz es el beneficio.
R. Martinho Fernandes

11
@SigTerm: a veces el código se debe mover a una función separada solo para mantener el tamaño de cada función en un tamaño fácil de razonar. Si no desea pagar una llamada de función, márquela forceinline.
Ben Voigt

257

Hay momentos en que usar gotoes realmente la respuesta CORRECTA, al menos para aquellos que no se crían en la creencia religiosa de que " gotonunca puede ser la respuesta, sin importar cuál sea la pregunta", y este es uno de esos casos.

Este código está usando el truco do { ... } while(0);con el único propósito de disfrazarse de a gotocomo a break. Si vas a usar goto, entonces sé abierto al respecto. No tiene sentido hacer que el código sea MÁS DURO de leer.

Una situación particular es cuando tienes mucho código con condiciones bastante complejas:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

En realidad, puede aclarar que el código es correcto al no tener una lógica tan compleja.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Editar: Para aclarar, de ninguna manera propongo el uso de gotocomo una solución general. Pero hay casos en que gotoes una mejor solución que otras soluciones.

Imagine, por ejemplo, que estamos recopilando algunos datos, y las diferentes condiciones que se están probando son una especie de "este es el final de los datos que se recopilan", que depende de algún tipo de marcadores de "continuar / finalizar" que varían según el lugar Estás en el flujo de datos.

Ahora, cuando hayamos terminado, debemos guardar los datos en un archivo.

Y sí, a menudo hay otras soluciones que pueden proporcionar una solución razonable, pero no siempre.


76
Discrepar. gotopuede tener un lugar, pero goto cleanupno lo tiene. La limpieza se realiza con RAII.
MSalters

14
@MSalters: Eso supone que la limpieza implica algo que se puede resolver con RAII. Tal vez debería haber dicho "error de problema" o algo así.
Mats Petersson el

25
Por lo tanto, seguir recibiendo votos negativos en este caso, presumiblemente de aquellos de la creencia religiosa, gotonunca es la respuesta correcta. Agradecería si hubiera un comentario ...
Mats Petersson

19
la gente odia gotoporque tienes que pensar en usarlo / para entender un programa que lo usa ... Los microprocesadores, por otro lado, están construidos jumpsy conditional jumps... entonces el problema es con algunas personas, no con la lógica o alguna otra cosa.
woliveirajr

17
+1 para el uso apropiado de goto. RAII no es la solución correcta si se esperan errores (no excepcionales), ya que eso sería un abuso de excepciones.
Joe

82

Puede usar un patrón de continuación simple con una boolvariable:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Esta cadena de ejecución se detendrá tan pronto como checkNregrese a false. No se realizarán más check...()llamadas debido al cortocircuito del &&operador. Por otra parte, los compiladores de optimización son lo suficientemente inteligente como para reconocer que el establecimiento goOnde falseuna calle de sentido único, e inserte el faltante goto endpara usted. Como resultado, el rendimiento del código anterior sería idéntico al de un do/ while(0), solo sin un doloroso golpe a su legibilidad.


30
Las tareas dentro de las ifcondiciones parecen muy sospechosas.
Mikhail

90
@Mikhail Solo para un ojo inexperto.
dasblinkenlight

20
He usado esta técnica antes y siempre me molestó que supuse que el compilador tendría que generar código para verificar cada uno, ifsin importar qué goOnocurriera, incluso si fallaba uno de los primeros (en lugar de saltar / romper) ... pero acabo de hacer una prueba y VS2012 al menos fue lo suficientemente inteligente como para cortocircuitar todo después del primer falso de todos modos. Usaré esto más a menudo. Nota: si se utiliza goOn &= checkN()a continuación, checkN()siempre funcionar incluso si goOnera falseal comienzo de la if(es decir, no hacer eso).
marca

11
@Nawaz: Si tiene una mente no entrenada haciendo cambios arbitrarios en una base de código, tiene un problema mucho mayor que solo las asignaciones dentro de ifs.
Ideal

66
¡@sisharp Elegance está tan en el ojo del espectador! No entiendo cómo en el mundo el mal uso de una construcción en bucle podría percibirse de alguna manera como "elegante", pero tal vez solo soy yo.
dasblinkenlight

38
  1. Intente extraer el código en una función separada (o quizás más de una). Luego regrese de la función si la verificación falla.

  2. Si está demasiado unido al código circundante para hacer eso, y no puede encontrar una manera de reducir el acoplamiento, mire el código después de este bloque. Presumiblemente, limpia algunos recursos utilizados por la función. Intente administrar estos recursos utilizando un objeto RAII ; luego reemplaza cada poco fiable breakcon return(o throw, si eso es más apropiado) y deja que el destructor del objeto te limpie.

  3. Si el flujo del programa es (necesariamente) tan escabroso que realmente necesitas un goto, entonces úsalo en lugar de darle un disfraz extraño.

  4. Si tiene reglas de codificación que lo prohíben ciegamente gotoy realmente no puede simplificar el flujo del programa, entonces probablemente tendrá que disfrazarlo con su dotruco.


44
Humildemente afirmo que RAII, aunque útil, no es una bala de plata. Cuando te encuentres a punto de escribir una clase convert-goto-to-RAII que no tenga otro uso, definitivamente creo que te servirá mejor simplemente usando el idioma "goto-end-of-the-world" que la gente ya mencionó.
idoby

1
@busy_wait: De hecho, RAII no puede resolver todo; Es por eso que mi respuesta no se detiene con el segundo punto, sino que continúa sugiriendo gotosi esa es realmente una mejor opción.
Mike Seymour

3
Estoy de acuerdo, pero creo que es una mala idea escribir clases de conversión goto-RAII y creo que debería declararse explícitamente.
idoby

@idoby, ¿qué hay de escribir una clase de plantilla RAII e instanciarla con cualquier limpieza de un solo uso que necesite en una lambda
Caleth

@Caleth me parece que estás reinventando ctors / dtors?
idoby

37

TLDR : RAII , código transaccional (solo establece resultados o devuelve cosas cuando ya está calculado) y excepciones.

Respuesta larga:

En C , la mejor práctica para este tipo de código es agregar una etiqueta EXIT / CLEANUP / other en el código, donde se realiza la limpieza de los recursos locales y se devuelve un código de error (si corresponde). Esta es la mejor práctica porque divide el código naturalmente en la inicialización, el cálculo, el compromiso y el retorno:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

En C, en la mayoría de bases de código, el if(error_ok != ...y gotocódigo está generalmente oculta detrás de unas macros convenientes ( RET(computation_result), ENSURE_SUCCESS(computation_result, return_code), etc.).

C ++ ofrece herramientas adicionales sobre C :

  • La funcionalidad del bloque de limpieza se puede implementar como RAII, lo que significa que ya no necesita el cleanupbloque completo y permite que el código del cliente agregue declaraciones de devolución anticipadas.

  • Lanzas cuando no puedes continuar, transformando todo if(error_ok != ...en llamadas directas.

Código C ++ equivalente:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

Esta es la mejor práctica porque:

  • Es explícito (es decir, si bien el manejo de errores no es explícito, el flujo principal del algoritmo sí lo es)

  • Es sencillo escribir código de cliente

  • Es minima

  • Es simple

  • No tiene construcciones de código repetitivo

  • No usa macros

  • No usa do { ... } while(0)construcciones raras

  • Es reutilizable con un esfuerzo mínimo (es decir, si quiero copiar la llamada a computation2();una función diferente, no tengo que asegurarme de agregar un do { ... } while(0)código nuevo, ni #defineuna macro de contenedor Goto y una etiqueta de limpieza, ni Algo más).


+1. Esto es lo que trato de usar, usando RAII o algo así. shared_ptrcon un eliminador personalizado puede hacer muchas cosas. Aún más fácil con lambdas en C ++ 11.
Macke

Es cierto, pero si termina usando shared_ptr para cosas que no son punteros, considere al menos escribirlo: namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };en este caso (con la make_handleconfiguración del tipo de borrador correcto en la construcción), el nombre del tipo ya no sugiere que sea un puntero .
utnapistim

21

Estoy agregando una respuesta en aras de la integridad. Varias otras respuestas señalaron que el bloque de condición grande podría dividirse en una función separada. Pero como también se señaló varias veces, este enfoque separa el código condicional del contexto original. Esta es una razón por la que se agregaron lambdas al lenguaje en C ++ 11. Otros sugirieron el uso de lambdas, pero no se proporcionó una muestra explícita. He puesto uno en esta respuesta. Lo que me sorprende es que se siente muy similar al do { } while(0)enfoque en muchos sentidos, y tal vez eso significa que sigue siendo un gotodisfraz ...

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations

77
Para mí, este truco se ve peor que el hacer ... mientras que el pirateo.
Michael

18

Ciertamente, no la respuesta, sino una respuesta (en aras de la integridad)

En vez de :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Podrías escribir:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Esto sigue siendo un goto disfrazado, pero al menos ya no es un bucle. Lo que significa que no tendrá que verificar con mucho cuidado que no haya algunos que continúen ocultos en algún lugar del bloque.

La construcción también es lo suficientemente simple como para que pueda esperar que el compilador la optimice.

Como lo sugiere @jamesdlin, incluso puedes ocultar eso detrás de una macro como

#define BLOC switch(0) case 0:

Y úsalo como

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Esto es posible porque la sintaxis del lenguaje C espera una declaración después de un cambio, no un bloque entre corchetes y puede poner una etiqueta de caso antes de esa declaración. Hasta ahora no veía el punto de permitir eso, pero en este caso particular es útil ocultar el interruptor detrás de una buena macro.


2
Ooh, listo Incluso podría ocultarlo detrás de una macro como define BLOCK switch (0) case 0:y usarlo como BLOCK { ... break; }.
jamesdlin

@jamesdlin: nunca antes se me había ocurrido que podría ser útil poner un estuche antes del soporte de apertura de un interruptor. Pero de hecho está permitido por C y en este caso es útil escribir una buena macro.
kriss

15

Recomendaría un enfoque similar a la respuesta de Mats menos lo innecesario goto. Solo ponga la lógica condicional en la función. Cualquier código que siempre se ejecute debe ir antes o después de invocar la función en la persona que llama:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}

3
Si tiene que adquirir otro recurso en el medio de func, necesita factorizar otra función (según su patrón). Si todas estas funciones aisladas necesitan los mismos datos, simplemente terminará copiando los mismos argumentos de pila una y otra vez, o decidirá asignar sus argumentos en el montón y pasar un puntero, sin aprovechar la característica más básica del lenguaje ( argumentos de función). En resumen, no creo que esta solución se adapte al peor de los casos en el que se verifican todas las condiciones después de adquirir nuevos recursos que deben limpiarse. Sin embargo, tenga en cuenta el comentario de Nawaz sobre lambdas.
TNE

2
No recuerdo que el OP haya dicho nada sobre la adquisición de recursos en el medio de su bloque de código, pero acepto que es un requisito factible. En ese caso, ¿qué tiene de malo declarar el recurso en la pila en cualquier lugar func()y permitir que su destructor se encargue de liberar recursos? Si algo fuera de func()necesita acceso al mismo recurso, debe declararse en el montón antes de llamar func()por un administrador de recursos adecuado.
Dan Bechard

12

El flujo de código en sí mismo ya es un olor a código que sucede mucho en la función. Si no hay una solución directa para eso (la función es una función de verificación general), entonces usar RAII para que pueda regresar en lugar de saltar a la sección final de la función podría ser mejor.


11

Si no necesita introducir variables locales durante la ejecución, a menudo puede aplanar esto:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()

2
Pero entonces cada condición debería incluir la anterior, que no solo es fea sino que también es potencialmente mala para el rendimiento. Es mejor saltar esos controles y limpieza de inmediato tan pronto como sepamos que "no estamos bien".
TNE

Eso es cierto, sin embargo, este enfoque tiene ventajas si hay cosas que deben hacerse al final, por lo que no desea regresar temprano (vea la edición). Si se combina con el enfoque de Denise de usar bools en las condiciones, entonces el impacto en el rendimiento será insignificante a menos que esto esté en un circuito muy cerrado.
the_mandrill

10

Similar a la respuesta de dasblinkenlight, pero evita la asignación dentro de la ifque podría ser "reparada" por un revisor de código:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

Utilizo este patrón cuando los resultados de un paso deben verificarse antes del siguiente paso, lo que difiere de una situación en la que todas las comprobaciones podrían realizarse por adelantado con un if( check1() && check2()...patrón de tipo grande .


Tenga en cuenta que check1 () realmente podría ser PerformStep1 () que devuelve un código de resultado del paso. Esto mantendría baja la complejidad de su función de flujo de proceso.
Denise Skidmore

10

Use excepciones. Su código se verá mucho más limpio (y se crearon excepciones exactamente para manejar errores en el flujo de ejecución de un programa). Para limpiar recursos (descriptores de archivos, conexiones de bases de datos, etc.), lea el artículo ¿Por qué C ++ no proporciona una construcción "finalmente"? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}

10
De Verdad? Primero, sinceramente, no veo ninguna mejora en la legibilidad, allí. NO USE EXCEPCIONES PARA CONTROLAR EL FLUJO DEL PROGRAMA. Ese NO ES su propósito apropiado. Además, las excepciones conllevan importantes penalizaciones de rendimiento. El caso de uso adecuado para una excepción es cuando existe alguna condición QUE NO PUEDE HACER NADA, como intentar abrir un archivo que ya está bloqueado exclusivamente por otro proceso, o falla una conexión de red, o falla una llamada a una base de datos , o una persona que llama pasa un parámetro no válido a un procedimiento. Esa clase de cosas. Pero NO use excepciones para controlar el flujo del programa.
Craig

3
Quiero decir, no ser un mocoso al respecto, pero ve a ese artículo de Stroustrup al que hiciste referencia y busca "¿Para qué no debo usar las excepciones?" sección. Entre otras cosas, dice: "En particular, throw no es simplemente una forma alternativa de devolver un valor de una función (similar a return). Hacerlo será lento y confundirá a la mayoría de los programadores de C ++ acostumbrados a ver excepciones utilizadas solo por error manejo. Del mismo modo, lanzar no es una buena manera de salir de un bucle ".
Craig

3
@Craig Todo lo que ha señalado es correcto, pero está asumiendo que el programa de muestra puede continuar después de que una check()condición falla, y eso es, sin duda, SU suposición, no hay contexto en la muestra. Asumiendo que el programa no puede continuar, usar excepciones es el camino a seguir.
Cartucho

2
Bueno, bastante cierto si ese es realmente el contexto. Pero, lo gracioso de los supuestos ... ;-)
Craig

10

Para mi do{...}while(0)esta bien. Si no desea ver el do{...}while(0), puede definir palabras clave alternativas para ellos.

Ejemplo:

SomeUtilities.hpp:

#define BEGIN_TEST do{
#define END_TEST }while(0);

SomeSourceFile.cpp:

BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;
   
   //processing code here

END_TEST

Creo que el compilador eliminará la while(0)condición innecesaria do{...}while(0)en versión binaria y convertirá los saltos en salto incondicional. Puede verificar su versión en lenguaje ensamblador para estar seguro.

El uso gototambién produce un código más limpio y es sencillo con la lógica de condición y luego salto. Puedes hacer lo siguiente:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;
   
   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.
 

Tenga en cuenta que la etiqueta se coloca después del cierre }. Este es el evitar un posible problema en el gotoque accidentalmente se coloca un código en el medio porque no vio la etiqueta. Ahora es como do{...}while(0)sin código de condición.

Para hacer este código más limpio y más comprensible, puede hacer esto:

SomeUtilities.hpp:

#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

SomeSourceFile.cpp:

BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

Con esto, puede hacer bloques anidados y especificar dónde desea salir / saltar.

BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);
  
      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

El código de espagueti no es gotoculpa del programador. Todavía puede producir código de espagueti sin usar goto.


10
Elegiría gotosobre extender la sintaxis del lenguaje usando el preprocesador un millón de veces.
Christian

2
"Para que este código sea más limpio y comprensible, puede [usar LOADS_OF_WEIRD_MACROS]" : no se calcula.
underscore_d

tome nota en la primera oración que do{...}while(0) se prefiere lo normal . esta respuesta es solo una sugerencia y un juego en macro / goto / label para romper bloques específicos.
acegs hace

8

Este es un problema bien conocido y bien resuelto desde una perspectiva de programación funcional: quizás la mónada.

En respuesta al comentario que recibí a continuación, he editado mi introducción aquí: puede encontrar todos los detalles sobre la implementación de mónadas C ++ en varios lugares que le permitirán lograr lo que sugiere Rotsor. Toma un tiempo asimilar mónadas, así que en su lugar voy a sugerir aquí un mecanismo rápido tipo mónada de "pobre hombre" para el que no necesita saber nada más que boost :: opcional.

Configure sus pasos de cálculo de la siguiente manera:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Obviamente, cada paso computacional puede hacer algo como regresar boost::nonesi el opcional que se le dio está vacío. Así por ejemplo:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Luego encadenarlos juntos:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

Lo bueno de esto es que puede escribir pruebas unitarias claramente definidas para cada paso computacional. Además, la invocación se lee como un inglés simple (como suele ser el caso con el estilo funcional).

Si no le importa la inmutabilidad y es más conveniente devolver el mismo objeto cada vez que pueda encontrar alguna variación usando shared_ptr o similar.


3
Este código tiene una propiedad no deseada de obligar a cada una de las funciones individuales a manejar la falla de la función anterior, por lo tanto, no utiliza correctamente el idioma Monad (donde se supone que los efectos monádicos, en este caso falla, se manejan implícitamente). Para hacer eso, debe tener en su optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);lugar y usar la operación de composición monádica ('bind') en lugar de la aplicación de función.
Rotsor

Gracias. Tienes razón: esa es la forma de hacerlo correctamente. No quería escribir demasiado en mi respuesta para explicar eso (de ahí el término "pobre hombre", que tenía la intención de sugerir que no iba a ser el cerdo aquí).
Benedicto

7

¿Qué hay de mover las declaraciones if a una función adicional que produce un resultado numérico o enum?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}

Esto es bueno cuando es posible, pero mucho menos general que la pregunta que se hace aquí. Cada condición podría depender del código ejecutado después de la última prueba de desramificación.
kriss

El problema aquí es que separas causa y consecuencia. Es decir, separa el código que se refiere al mismo número de condición y esto puede ser una fuente de errores adicionales.
Riga

@kriss: Bueno, la función ConditionCode () podría ajustarse para encargarse de esto. El punto clave de esa función es que puede usar return <result> para una salida limpia tan pronto como se haya calculado una condición final; Y eso es lo que proporciona claridad estructural aquí.
karx11erx

@Riga: Imo, estas son objeciones completamente académicas. Encuentro que C ++ se vuelve más complejo, críptico e ilegible con cada nueva versión. No puedo ver un problema con una pequeña función auxiliar que evalúa condiciones complejas de una manera bien estructurada para hacer que la función objetivo justo debajo sea más legible.
karx11erx

1
@ karx11erx mis objeciones son prácticas y se basan en mi experiencia. Este patrón es malo sin relación con C ++ 11 o cualquier lenguaje. Si tiene dificultades con las construcciones de lenguaje que le permiten escribir una buena arquitectura, eso no es un problema de lenguaje.
Riga

5

Algo como esto tal vez

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

o use excepciones

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

Usando excepciones también puede pasar datos.


10
Por favor, no haga cosas inteligentes como su definición de siempre, generalmente hacen que el código sea más difícil de leer para otros desarrolladores. He visto a alguien definir Case como break; case en un archivo de encabezado, y lo usé en un switch en un archivo cpp, haciendo que otros se pregunten durante horas por qué el switch se rompe entre las declaraciones de Case. Grrr ...
Michael

55
Y cuando nombre las macros, debe hacer que se vean como macros (es decir, en mayúsculas). De lo contrario, alguien que nombre una variable / función / tipo / etc. named everserá muy infeliz ...
jamesdlin

5

No estoy particularmente en la forma de usar breako returnen tal caso. Dado que normalmente cuando nos enfrentamos a una situación así, generalmente es un método relativamente largo.

Si tenemos múltiples puntos de salida, puede causar dificultades cuando queremos saber qué causará que se ejecute cierta lógica: normalmente seguimos subiendo bloques que encierran esa pieza de lógica, y los criterios de esos bloques que nos rodean nos dicen el situación:

Por ejemplo,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

Al observar los bloques que encierran, es fácil descubrir que myLogic()solo suceden cuando conditionA and conditionB and conditionCes cierto.

Se vuelve mucho menos visible cuando hay retornos tempranos:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

Ya no podemos navegar hacia arriba myLogic(), mirando el bloque adjunto para descubrir la condición.

Hay diferentes soluciones que usé. Aqui esta uno de ellos:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(Por supuesto, es bienvenido usar la misma variable para reemplazar todo isA isB isC).

Tal enfoque al menos le dará al lector de código, que myLogic()se ejecuta cuando isB && conditionC. Se le da al lector una pista de que necesita buscar más a fondo lo que hará que B sea cierto.


3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

¿Qué hay sobre eso?


el if es obsoleto, solo return condition;, de lo contrario, creo que esto se puede mantener bien.
SpaceTrucker

2

Otro patrón útil si necesita diferentes pasos de limpieza dependiendo de dónde esté la falla:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }

2

No soy un programador de C ++ , así que no escribiré ningún código aquí, pero hasta ahora nadie ha mencionado una solución orientada a objetos. Así que aquí está mi suposición al respecto:

Tener una interfaz genérica que proporcione un método para evaluar una sola condición. Ahora puede usar una lista de implementaciones de esas condiciones en su objeto que contiene el método en cuestión. Usted itera sobre la lista y evalúa cada condición, posiblemente rompiendo temprano si falla una.

Lo bueno es que dicho diseño se adhiere muy bien al principio abierto / cerrado , porque puede agregar fácilmente nuevas condiciones durante la inicialización del objeto que contiene el método en cuestión. Incluso puede agregar un segundo método a la interfaz con el método para la evaluación de la condición que devuelve una descripción de la condición. Esto se puede utilizar para sistemas de autodocumentación.

La desventaja, sin embargo, es que hay un poco más de sobrecarga involucrada debido al uso de más objetos y la iteración sobre la lista.


¿Podría agregar un ejemplo en un idioma diferente? Creo que esta pregunta se aplica a muchos lenguajes, aunque se hizo específicamente sobre C ++.
Denise Skidmore el

1

Así es como lo hago.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}

1

Primero, un breve ejemplo para mostrar por qué gotono es una buena solución para C ++:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

Intente compilar esto en un archivo de objeto y vea qué sucede. Luego prueba el equivalente do+ break+while(0) .

Eso fue un aparte. El punto principal sigue.

Esos pequeños fragmentos de código a menudo requieren algún tipo de limpieza en caso de que falle toda la función. Esas limpiezas por lo general quieren que sucedan en el orden opuesto de los trozos mismos, ya que "desenrolla" el cálculo parcialmente terminado.

Una opción para obtener esta semántica es RAII ; ver la respuesta de @ utnapistim. C ++ garantiza que los destructores automáticos se ejecutan en el orden opuesto a los constructores, lo que naturalmente proporciona un "desenrollado".

Pero eso requiere muchas clases de RAII. A veces, una opción más simple es usar la pila:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...y así. Esto es fácil de auditar, ya que coloca el código "deshacer" al lado del código "hacer". La auditoría fácil es buena. También hace que el control de flujo sea muy claro. También es un patrón útil para C.

Puede requerir que las calcfunciones tomen muchos argumentos, pero eso generalmente no es un problema si sus clases / estructuras tienen buena cohesión. (Es decir, las cosas que pertenecen juntas viven en un solo objeto, por lo que estas funciones pueden tomar punteros o referencias a un pequeño número de objetos y aún hacer mucho trabajo útil).


Muy fácil auditar la ruta de limpieza, pero tal vez no sea tan fácil rastrear la ruta dorada. Pero en general, creo que algo como esto alienta un patrón de limpieza consistente.
Denise Skidmore

0

Si el código tiene un bloque largo de sentencias if..else if..else, puede intentar reescribir todo el bloque con la ayuda de Functorso function pointers. Puede que no sea la solución correcta siempre, pero a menudo lo es.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html


En principio esto es posible (y no está mal), pero con objetos de función explícitos o punteros simplemente interrumpe el flujo del código con demasiada fuerza. OTOH el uso, aquí equivalente, de lambdas o funciones con nombre ordinario es una buena práctica, eficiente y se lee bien.
Leftaroundabout

0

Me sorprende la cantidad de respuestas diferentes que se presentan aquí. Pero, finalmente, en el código que tengo que cambiar (es decir, eliminar este do-while(0)truco o algo), hice algo diferente de cualquiera de las respuestas que se mencionan aquí y estoy confundido por qué nadie pensó esto. Esto es lo que hice:

Código inicial:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Ahora:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

¡Entonces, lo que se ha hecho aquí es que las cosas de acabado se han aislado en una función y las cosas se han vuelto tan simples y limpias de repente!

Pensé que valía la pena mencionar esta solución, así que proporcione aquí.


0

Consolidarlo en una sola ifdeclaración:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

Este es el patrón utilizado en lenguajes como Java donde se eliminó la palabra clave goto.


2
Eso solo funciona si no tiene que hacer nada entre las pruebas de condición. (bueno, supongo que podría ocultar el hacer cosas dentro de algunas llamadas a funciones realizadas por las pruebas de condición, pero eso podría
ofuscarse

@JeremyFriesner En realidad, podrías hacer las cosas intermedias como funciones booleanas separadas que siempre se evalúan como true. La evaluación de cortocircuito garantizaría que nunca ejecute cosas intermedias para las cuales no pasaron todas las pruebas de requisitos previos.
AJMansfield

@AJMansfield sí, a eso me refería en mi segunda oración ... pero no estoy seguro de que eso sea una mejora en la calidad del código.
Jeremy Friesner

@JeremyFriesner Nada le impide escribir las condiciones (/*do other stuff*/, /*next condition*/), incluso podría formatearlo bien. Simplemente no esperes que a la gente le guste. Pero, honestamente, esto sólo sirve para demostrar que fue un error que Java fase que gotola declaración ...
cmaster - Restablecer Monica

@JeremyFriesner Supuse que eran booleanos. Si las funciones se ejecutaran dentro de cada condición, entonces hay una mejor manera de manejarla.
Tyzoid

0

Si usa el mismo controlador de errores para todos los errores, y cada paso devuelve un valor bool que indica éxito:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(Similar a la respuesta de tyzoid, pero las condiciones son las acciones, y el && evita que ocurran acciones adicionales después de la primera falla).


0

¿Por qué no se respondió el método de marcado? Se utiliza desde hace siglos.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
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.