¿Cómo evitar las cadenas "si"?


266

Suponiendo que tengo este pseudocódigo:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

Las funciones executeStepXdeben ejecutarse si y solo si las anteriores tienen éxito. En cualquier caso, la executeThisFunctionInAnyCasefunción debe llamarse al final. Soy un novato en programación, lo siento por la pregunta muy básica: ¿hay alguna manera (en C / C ++, por ejemplo) para evitar tanto tiempo?if cadena produzca ese tipo de "pirámide de código", a expensas de la legibilidad del código? ?

Sé que si pudiéramos omitir la executeThisFunctionInAnyCasellamada a la función, el código podría simplificarse como:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Pero la restricción es la executeThisFunctionInAnyCasellamada a la función. ¿Podría la breakdeclaración ser utilizada de alguna manera?


254
@ FrédéricHamidi mal mal mal mal! ¡Nunca digas que conducir tu flujo de programa con excepciones es bueno! Las excepciones definitivamente NO son adecuadas para este propósito, por demasiadas razones.
Piotr Zierhoffer

26
@Piotr, Python me echó a perder (lo que en realidad alienta esto). Sé que no se supone que las excepciones se usen para el control de flujo en C ++, pero ¿es realmente el control de flujo aquí? ¿No podría considerarse que una función que regresa falsees similar a una situación excepcional?
Frédéric Hamidi

13
Eso depende de la semántica de un programa. Un falseretorno puede ser bastante normal.
dornhege

28
He revertido su pregunta a su primera revisión. No debe cambiar su pregunta radicalmente después de haber recibido un cierto número de preguntas (> 0), porque eso invalidará todas las respuestas dadas hasta ese momento y creará confusión. Abra una nueva pregunta en su lugar.
Zapato

13
Deseo que todos los "programadores novatos" hagan preguntas de diseño como esta.
Jezen Thomas

Respuestas:


486

Puede usar un &&(lógico Y):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

Esto satisfará sus dos requisitos:

  • executeStep<X>()debe evaluar solo si el anterior tuvo éxito (esto se llama evaluación de cortocircuito )
  • executeThisFunctionInAnyCase() se ejecutará en cualquier caso

29
Esto es semánticamente correcto (de hecho, queremos que TODAS las condiciones sean verdaderas) así como una muy buena técnica de programación (evaluación de cortocircuito). Además, esto se puede usar en cualquier situación compleja en la que crear funciones estropee el código.
Sanchises

58
@RobAu: Entonces será bueno para los programadores junior ver finalmente el código que se basa en la evaluación abreviada y puede incitarlos a abordar este tema, lo que a su vez los ayudaría a convertirse en programadores senior. Claramente un win-win: código decente y alguien aprendió algo al leerlo.
x4u

24
Esta debería ser la respuesta principal
Nombre para mostrar

61
@RobAu: Este no es un truco que aprovecha un truco oscuro de sintaxis, es muy idiomático en casi todos los lenguajes de programación, hasta el punto de ser una práctica estándar indestructible.
BlueRaja - Danny Pflughoeft

38
Esta solución solo se aplica si las condiciones son en realidad llamadas simples de funciones. En el código real, esas condiciones pueden tener de 2 a 5 líneas de largo (y ser combinaciones de muchas otras ) &&y, ||por lo tanto, no hay forma de que desee unirlas en una sola ifdeclaración sin atornillar la legibilidad. Y no siempre es fácil mover esas condiciones a funciones externas, ya que pueden depender de muchas variables locales calculadas previamente, lo que crearía un desastre terrible si intenta pasar cada una como argumento individual.
Hamstergene

358

Simplemente use una función adicional para que su segunda versión funcione:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

El uso de ifs profundamente anidados (su primera variante) o el deseo de salir de "parte de una función" generalmente significa que necesita una función adicional.


51
+1 finalmente alguien publicó una respuesta sensata. Esta es la forma más correcta, segura y legible, en mi opinión.
Lundin

31
+1 Esta es una buena ilustración del "Principio de responsabilidad única". La función foofunciona a través de una secuencia de condiciones y acciones relacionadas. La función barestá limpiamente separada de las decisiones. Si viéramos los detalles de las condiciones y acciones, podría resultar que footodavía se está haciendo demasiado, pero por ahora esta es una buena solución.
GraniteRobert

13
La desventaja es que C no tiene funciones anidadas, por lo que si esos 3 pasos necesarios para usar variables de barusted tendrían que pasarlas manualmente foocomo parámetros. Si ese es el caso y foosolo se llama una vez, me equivocaría al usar la versión goto para evitar definir dos funciones estrechamente acopladas que terminarían no siendo muy reutilizables.
hugomg

77
No estoy seguro de la sintaxis de cortocircuito para C, pero en C # foo () podría escribirse comoif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

66
@ user1598390 Los Goto se usan todo el tiempo, especialmente en la programación de sistemas cuando necesita desenrollar una gran cantidad de código de configuración.
Scotty Bauer

166

Los programadores de la vieja escuela C usan gotoen este caso. Es el único uso gotoque en realidad es alentado por la guía de estilo de Linux, se llama la salida de función centralizada:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Algunas personas evitan el uso gotoenvolviendo el cuerpo en un bucle y rompiéndolo, pero efectivamente ambos enfoques hacen lo mismo. El gotoenfoque es mejor si necesita alguna otra limpieza solo si executeStepA()fue exitosa:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

Con el enfoque de bucle, terminaría con dos niveles de bucles en ese caso.


108
+1. Mucha gente ve gotoe inmediatamente piensa "Este es un código terrible" pero tiene sus usos válidos.
Graeme Perrow

46
Excepto que este es un código bastante desordenado, desde el punto de vista del mantenimiento. Particularmente cuando hay varias etiquetas, donde el código también se vuelve más difícil de leer. Hay formas más elegantes de lograr esto: usar funciones.
Lundin

29
-1 Veo el gotoy ni siquiera tengo que pensar para ver que este es un código terrible. Una vez tuve que mantener eso, es muy desagradable. El OP sugiere una alternativa razonable en lenguaje C al final de la pregunta, e incluí eso en mi respuesta.
Saludos y hth. - Alf

56
No hay nada de malo en este uso limitado y autónomo de goto. Pero cuidado, el goto es una droga de entrada y si no tienes cuidado, un día te darás cuenta de que nadie más come espagueti con los pies, y lo has estado haciendo durante 15 años, comenzando después de que pensaste "Puedo arreglar esto de lo contrario, un error de pesadilla con una etiqueta ... "
Tim Post

66
He escrito probablemente cien mil líneas de código extremadamente claro y fácil de mantener en este estilo. Las dos cosas clave para entender son (1) disciplina! establecer una directriz clara para el diseño de cada función y solo violarla en caso de extrema necesidad, y (2) comprender que lo que estamos haciendo aquí es simular excepciones en un lenguaje que no las tiene . throw¡es en muchos aspectos peor que gotoporque con el contexto localthrow ni siquiera está claro dónde vas a terminar! Use las mismas consideraciones de diseño para el flujo de control de estilo de goto que lo haría para las excepciones.
Eric Lippert

131

Esta es una situación común y hay muchas formas comunes de tratarla. Aquí está mi intento de una respuesta canónica. Comenta si me perdí algo y mantendré esta publicación actualizada.

Esta es una flecha

Lo que está discutiendo se conoce como el antipatrón de flecha . Se llama flecha porque la cadena de ifs anidados forma bloques de código que se expanden más y más hacia la derecha y luego hacia la izquierda, formando una flecha visual que "apunta" al lado derecho del panel del editor de código.

Aplana la flecha con la guardia

Aquí se discuten algunas formas comunes de evitar la flecha . El método más común es usar un patrón de protección , en el que el código maneja los flujos de excepción primero y luego maneja el flujo básico, por ejemplo, en lugar de

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... usarías ...

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

Cuando hay una larga serie de guardias, esto aplana el código considerablemente ya que todos los guardias aparecen completamente a la izquierda y sus ifs no están anidados. Además, está visualmente emparejando la condición lógica con su error asociado, lo que hace que sea mucho más fácil saber lo que está sucediendo:

Flecha:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

Guardia:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

Esto es objetiva y cuantificablemente más fácil de leer porque

  1. Los caracteres {y} para un bloque lógico dado están más juntos
  2. La cantidad de contexto mental necesaria para comprender una línea particular es menor
  3. La totalidad de la lógica asociada con una condición if es más probable que esté en una página
  4. La necesidad de que el codificador desplace la página / pista ocular disminuye considerablemente

Cómo agregar código común al final

El problema con el patrón de guardia es que se basa en lo que se llama "retorno oportunista" o "salida oportunista". En otras palabras, rompe el patrón de que todas y cada una de las funciones deben tener exactamente un punto de salida. Este es un problema por dos razones:

  1. A algunas personas les molesta, por ejemplo, las personas que aprendieron a codificar en Pascal han aprendido que una función = un punto de salida.
  2. No proporciona una sección de código que se ejecute al salir, pase lo que pase , cuál es el tema en cuestión.

A continuación, proporcioné algunas opciones para solucionar esta limitación mediante el uso de funciones de idioma o evitando el problema por completo.

Opción 1. No puede hacer esto: use finally

Desafortunadamente, como desarrollador de c ++, no puedes hacer esto. Pero esta es la respuesta número uno para los idiomas que contienen finalmente una palabra clave, ya que esto es exactamente para lo que sirve.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

Opción 2. Evita el problema: reestructura tus funciones

Puede evitar el problema dividiendo el código en dos funciones. Esta solución tiene la ventaja de funcionar para cualquier idioma y, además, puede reducir la complejidad ciclomática , que es una forma comprobada de reducir la tasa de defectos y mejora la especificidad de cualquier prueba unitaria automatizada.

Aquí hay un ejemplo:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Opción 3. Truco de idioma: usa un bucle falso

Otro truco común que veo es usar while (verdadero) y break, como se muestra en las otras respuestas.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

Si bien esto es menos "honesto" que el uso goto, es menos propenso a equivocarse al refactorizar, ya que marca claramente los límites del alcance lógico. ¡Un ingenuo programador que corta y pega sus etiquetas o sus gotodeclaraciones puede causar grandes problemas! (Y, francamente, el patrón es tan común ahora que creo que comunica claramente la intención y, por lo tanto, no es "deshonesto" en absoluto).

Hay otras variantes de estas opciones. Por ejemplo, uno podría usar en switchlugar de while. Cualquier construcción de lenguaje con una breakpalabra clave probablemente funcionaría.

Opción 4. Aproveche el ciclo de vida del objeto

Otro enfoque aprovecha el ciclo de vida del objeto. Use un objeto de contexto para transportar sus parámetros (algo de lo que nuestro ingenuo ejemplo carece sospechosamente) y deséchelo cuando haya terminado.

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Nota: Asegúrese de comprender el ciclo de vida del objeto de su idioma de elección. Necesitas algún tipo de recolección de basura determinista para que esto funcione, es decir, debes saber cuándo se llamará al destructor. En algunos idiomas deberá usar en Disposelugar de un destructor.

Opción 4.1. Aproveche el ciclo de vida del objeto (patrón de envoltura)

Si va a utilizar un enfoque orientado a objetos, puede hacerlo bien. Esta opción utiliza una clase para "ajustar" los recursos que requieren limpieza, así como sus otras operaciones.

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Nuevamente, asegúrese de comprender el ciclo de vida de su objeto.

Opción 5. Truco del lenguaje: usar evaluación de cortocircuito

Otra técnica es aprovechar la evaluación de cortocircuito .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

Esta solución aprovecha la forma en que funciona el operador &&. Cuando el lado izquierdo de && se evalúa como falso, el lado derecho nunca se evalúa.

Este truco es más útil cuando se requiere un código compacto y cuando no es probable que el código vea mucho mantenimiento, por ejemplo, está implementando un algoritmo bien conocido. Para una codificación más general, la estructura de este código es demasiado frágil; Incluso un cambio menor en la lógica podría desencadenar una reescritura total.


12
¿Finalmente? C ++ no tiene una cláusula finalmente. Un objeto con un destructor se usa en situaciones en las que cree que necesita una cláusula finalmente.
Bill Door

1
Editado para acomodar los dos comentarios anteriores.
John Wu

2
Con el código trivial (por ejemplo, en mi ejemplo), los patrones anidados son probablemente más comprensibles. Con el código del mundo real (que podría abarcar varias páginas), el patrón de protección es objetivamente más fácil de leer porque requerirá menos desplazamiento y menos seguimiento ocular, por ejemplo, la distancia promedio de {a} es más corta.
John Wu

1
He visto un patrón anidado donde el código ya no era visible en una pantalla de 1920 x 1080 ... Intente averiguar qué código de manejo de errores se realizará si falla la tercera acción ... He usado do {...} mientras (0) en su lugar para que no necesite el descanso final (por otro lado, mientras que (verdadero) {...} permite que "continuar" comience de nuevo.
gnasher729

2
Su opción 4 es una pérdida de memoria en C ++ en realidad (ignorando el error de sintaxis menor). Uno no usa "nuevo" en este tipo de casos, solo diga "MyContext myContext;".
Sumudu Fernando

60

Solo haz

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

Es así de simple.


Debido a tres ediciones en las que cada una ha cambiado la pregunta de manera fundamental (cuatro si una cuenta la revisión de nuevo a la versión # 1), incluyo el ejemplo de código al que estoy respondiendo:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

14
Respondí esto (de manera más concisa) en respuesta a la primera versión de la pregunta, y recibió 20 votos a favor antes de que Bill the Lizard lo eliminara, después de algunos comentarios y edición de Darkness Races en órbita.
Saludos y hth. - Alf

1
@ Cheersandhth-Alf: No puedo creer que haya sido eliminado por mod. Eso es muy malo. (+1)
El cruasán paramagnético

2
¡Mi +1 a tu coraje! (3 veces importa: D).
Hacks

Es completamente importante que los programadores novatos aprendan cosas como la ejecución de órdenes con múltiples "ands" booleanos, cómo eso es diferente en diferentes idiomas, etc. Y esta respuesta es un gran indicador de eso. Pero es solo un "no iniciador" en la programación comercial normal. No es simple, no es mantenible, no es modificable. Claro, si solo está escribiendo un "código de vaquero" rápido para usted, haga esto. Pero simplemente no tiene "ninguna conexión con la ingeniería de software cotidiana como se practica hoy en día". Por cierto, perdón por la ridícula "edición-confusión" que tuviste que soportar aquí, @cheers :)
Fattie

1
@ JoeBlow: Estoy con Alf en esto, encuentro la &&lista mucho más clara. Comúnmente rompo las condiciones en líneas separadas y agrego un final // explanationa cada una ... Al final, es mucho menos código para mirar, y una vez que entiendes cómo &&funciona, no hay un esfuerzo mental continuo. Mi impresión es que la mayoría de los programadores profesionales de C ++ estarían familiarizados con esto, pero como usted dice en diferentes industrias / proyectos, el enfoque y la experiencia difieren.
Tony Delroy

35

En realidad, hay una manera de diferir acciones en C ++: haciendo uso del destructor de un objeto.

Suponiendo que tiene acceso a C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

Y luego usando esa utilidad:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

67
Esto es una completa ofuscación para mí. Realmente no entiendo por qué a tantos programadores de C ++ les gusta resolver un problema lanzando tantas características del lenguaje como sea posible, hasta el punto en que todos se han olvidado del problema que realmente estaban resolviendo: ya no les importa, porque son ahora tan interesado en usar todas esas características de lenguaje exótico. Y a partir de ahí, puede mantenerse ocupado durante días y semanas escribiendo metacódigo, luego manteniendo el metacódigo y luego escribiendo metacódigo para manejar el metacódigo.
Lundin

24
@Lundin: Bueno, no entiendo cómo uno puede estar satisfecho con el código frágil que se romperá tan pronto como se introduzca una continuación / interrupción / retorno temprano o se arroje una excepción. Por otro lado, esta solución es resistente al mantenimiento futuro, y solo se basa en el hecho de que los destructores se ejecutan durante el desenrollado, que es una de las características más importantes de C ++. Calificar esto de exótico mientras que es el principio subyacente que impulsa todos los contenedores estándar es divertido, por decir lo menos.
Matthieu M.

77
@Lundin: El beneficio del código de Matthieu es que executeThisFunctionInAnyCase();se ejecutará incluso si foo();arroja una excepción. Al escribir código seguro de excepción, es una buena práctica poner todas esas funciones de limpieza en un destructor.
Brian

66
@Brian Entonces no arrojes ninguna excepción foo(). Y si lo haces, atrápalo. Problema resuelto. Solucione errores solucionándolos, no escribiendo soluciones alternativas.
Lundin

18
@Lundin: la Deferclase es una pequeña pieza de código reutilizable que le permite realizar cualquier limpieza de final de bloque, de una manera segura y excepcional. se conoce más comúnmente como guardia de alcance . sí, cualquier uso de un protector de alcance puede expresarse de otras maneras más manuales, al igual que cualquier forbucle puede expresarse como un bloque y un whilebucle, que a su vez puede expresarse con ify goto, que puede expresarse en lenguaje ensamblador si lo desea, o para aquellos que son verdaderos maestros, alterando los bits en la memoria a través de rayos cósmicos dirigidos por el efecto mariposa de gruñidos y cánticos cortos especiales. Pero, ¿por qué hacer eso?
Saludos y hth. - Alf

34

Hay una buena técnica que no necesita una función de envoltura adicional con las declaraciones de retorno (el método prescrito por Itjax). Hace uso de un while(0)pseudo-loop do . Esto while (0)asegura que en realidad no es un bucle sino que se ejecuta solo una vez. Sin embargo, la sintaxis del bucle permite el uso de la instrucción break.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

11
Usar una función con múltiples retornos es similar, pero en mi opinión es más legible.
Lundin

44
Sí, definitivamente es más legible ... sin embargo, desde el punto de vista de la eficiencia, se puede evitar la sobrecarga de la llamada de función adicional (paso y retorno de parámetros) con la construcción do {} while (0).
Debasis

55
Todavía eres libre de hacer la función inline. De todos modos, esta es una buena técnica para saber, porque ayuda en algo más que este problema.
Ruslan

2
@Lundin Tienes que tener en cuenta la localidad del código, distribuir el código en muchos lugares también tiene sus problemas.
API-Beast

3
En mi experiencia, este es un idioma muy inusual. Me llevaría un tiempo descubrir qué diablos estaba pasando, y eso es una mala señal cuando estoy revisando el código. Dado que parece no tener ventajas sobre los otros, enfoques más comunes y, por lo tanto, más legibles, no pude cerrar la sesión.
Cody Gray

19

También puedes hacer esto:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

De esta manera, tiene un tamaño de crecimiento lineal mínimo, +1 línea por llamada, y es fácil de mantener.


EDITAR : (Gracias @Unda) No es un gran fan porque pierdes visibilidad IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
Se trataba de las llamadas a funciones accidentales dentro de push_back (), pero de todos modos lo solucionó :)
Quentin

44
Me encantaría saber por qué esto recibió un voto negativo. Suponiendo que los pasos de ejecución son realmente simétricos, como parecen ser, entonces es limpio y fácil de mantener.
ClickRick

1
Aunque esto podría parecer más limpio. ¡Puede ser más difícil de entender para las personas, y definitivamente es más difícil de entender para el compilador!
Roy T.

1
Tratar sus funciones más como datos a menudo es una buena idea: una vez que lo haya hecho, también notará refactorizaciones posteriores. Aún mejor es donde cada pieza que está manejando es una referencia de objeto, no solo una referencia de función, esto le dará aún más capacidad para mejorar su código en el futuro.
Bill K

3
Ligeramente sobre-diseñado para un caso trivial, pero esta técnica definitivamente tiene una buena característica que otros no tienen: puede cambiar el orden de ejecución y [número de] las funciones llamadas en tiempo de ejecución, y eso es bueno :)
Xocoatzin

18

¿Funcionaría esto? Creo que esto es equivalente a tu código.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
Normalmente llamo a la variable okcuando uso la misma variable como esta.
Macke

1
Estaría muy interesado en saber por qué los votos negativos. ¿Qué está pasando mal aquí?
ABCplus

1
compare su respuesta con el enfoque de corto circuito para la complejidad ciclomática.
AlphaGoku

14

Asumiendo que el código deseado es como lo veo actualmente:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

Diría que el enfoque correcto, ya que es el más simple de leer y el más fácil de mantener, tendría menos niveles de sangría, que es (actualmente) el propósito declarado de la pregunta.

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Esto evita cualquier necesidad de gotos, excepciones, whilebucles ficticios u otras construcciones difíciles y simplemente continúa con el simple trabajo en cuestión.


Cuando se usan bucles, generalmente es aceptable usar returny breaksaltar fuera del bucle sin necesidad de introducir variables adicionales de "bandera". En este caso, usar un goto sería igualmente inocuo: recuerde que está cambiando la complejidad de goto adicional por una complejidad variable mutable adicional.
hugomg

2
@hugomg Las variables estaban en la pregunta original. No hay complejidad adicional aquí. Se hicieron suposiciones sobre la pregunta (por ejemplo, que las variables son necesarias en el código redactado), por lo que se conservaron. Si no son necesarios, entonces el código puede simplificarse, pero dada la naturaleza incompleta de la pregunta, no hay otra suposición que pueda hacerse válidamente.
ClickRick

Enfoque muy útil, especialmente. para uso de una persona profesa newbie, proporciona una solución más limpia sin inconvenientes. Noto que tampoco depende de stepstener la misma firma o incluso de ser funciones en lugar de bloques. Puedo ver que esto se usa como un refactor de primer paso, incluso cuando un enfoque más sofisticado es válido.
Keith

12

¿Podría la declaración de ruptura ser utilizada de alguna manera?

Tal vez no sea la mejor solución, pero puede poner sus declaraciones en un do .. while (0)bucle y usar breakdeclaraciones en lugar de return.


2
No fui yo quien lo rechazó, pero esto sería un abuso de una construcción en bucle para algo donde el efecto es lo que actualmente se desea pero inevitablemente resultaría en dolor. Probablemente para el próximo desarrollador que tenga que mantenerlo dentro de 2 años después de que haya pasado a otro proyecto.
ClickRick

3
@ClickRick usando do .. while (0)para definiciones de macro también está abusando de los bucles, pero se considera correcto.
ouah

1
Quizás, pero hay formas más limpias de lograrlo.
ClickRick

44
@ClickRick, el único propósito de mi respuesta es responder. Podría utilizarse la declaración de ruptura de alguna manera y la respuesta es sí, las primeras palabras en mi respuesta sugieren que esta puede no ser la solución.
ouah

2
Esta respuesta debería ser solo un comentario
msmucker0527

12

Podrías poner todo el if condiciones, formateadas como lo desee en una función propia, y al regresar ejecute elexecuteThisFunctionInAnyCase() función.

Desde el ejemplo base en el OP, la prueba de condición y la ejecución se pueden dividir como tal;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

Y luego llamado como tal;

InitialSteps();
executeThisFunctionInAnyCase();

Si las lambdas de C ++ 11 están disponibles (no había una etiqueta de C ++ 11 en el OP, pero aún pueden ser una opción), entonces podemos renunciar a la función separada y envolver esto en una lambda.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

Si no le gustan gotoy no le gustan los do { } while (0);bucles y le gusta usar C ++, también puede usar una lambda temporal para tener el mismo efecto.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

1
if le disgusta ir && a no le gusta hacer {} mientras (0) && le gusta C ++ ... Lo siento, no pude resistir, pero esa última condición falla porque la pregunta está etiquetada con c y c ++
ClickRick

@ClickRick siempre es difícil complacer a todos. En mi opinión, no existe tal cosa como C / C ++ que usualmente codifica en cualquiera de esos y el uso del otro está mal visto.
Alex

9

Las cadenas de IF / ELSE en su código no son el problema del idioma, sino el diseño de su programa. Si puede refactorizar o reescribir su programa, me gustaría sugerir que busque en Patrones de diseño ( http://sourcemaking.com/design_patterns ) para encontrar una mejor solución.

Por lo general, cuando ve muchos IF y otros en su código, es una oportunidad para implementar el Patrón de diseño de estrategia ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) o tal vez una combinación de otros patrones.

Estoy seguro de que hay alternativas para escribir una larga lista de if / else, pero dudo que cambien algo, excepto que la cadena se verá bonita para ti (Sin embargo, la belleza está en el ojo del espectador aún se aplica al código también:-) ) . Debería preocuparse por cosas como (en 6 meses cuando tengo una nueva condición y no recuerdo nada sobre este código, ¿podré agregarlo fácilmente? ¿O qué pasa si la cadena cambia, qué tan rápido y sin errores? voy a implementarlo)


9

Solo haz esto ...

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 veces de 100, esta es la única forma de hacerlo.

Nunca, nunca, intentes hacer algo "complicado" en el código de la computadora.


Por cierto, estoy bastante seguro de que el siguiente es el solución real que tenía en mente ...

La declaración de continuación es crítica en la programación algorítmica. (De manera similar, la declaración goto es crítica en la programación algorítmica).

En muchos lenguajes de programación puedes hacer esto:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Tenga en cuenta en primer lugar: los bloques desnudos como ese ejemplo son una parte crítica e importante de escribir código hermoso, particularmente si se trata de programación "algorítmica").

De nuevo, eso es exactamente lo que tenías en la cabeza, ¿verdad? Y esa es la hermosa manera de escribirlo, así que tienes buenos instintos.

Sin embargo, trágicamente, en la versión actual de Objective-C (aparte: no sé sobre Swift, lo siento) hay una característica risible en la que se comprueba si el bloque de cierre es un bucle.

ingrese la descripción de la imagen aquí

Así es como lo evitas ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Así que no olvides eso ...

hacer {} while (falso);

solo significa "hacer este bloque una vez".

es decir, no hay absolutamente ninguna diferencia entre escribir do{}while(false);y simplemente escribir {}.

Esto ahora funciona perfectamente como quería ... aquí está la salida ...

ingrese la descripción de la imagen aquí

Entonces, es posible que así sea como veas el algoritmo en tu cabeza. Siempre debes tratar de escribir lo que tienes en la cabeza. (Particularmente si no estás sobrio, ¡porque es cuando sale lo bonito! :))

En proyectos "algorítmicos" donde esto sucede mucho, en el objetivo-c, siempre tenemos una macro como ...

#define RUNONCE while(false)

... entonces puedes hacer esto ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

Hay dos puntos:

a, aunque es estúpido que el objetivo-c verifique el tipo de bloque en el que se encuentra una declaración de continuación, es preocupante "luchar contra eso". Entonces es una decisión difícil.

b, está la pregunta, ¿debería sangrar, en el ejemplo, ese bloque? Pierdo el sueño por preguntas como esa, así que no puedo aconsejar.

Espero eso ayude.



¿Pusiste la recompensa para obtener más reputación? :)
TMS

En lugar de poner todos esos comentarios en el if, también podría usar nombres de funciones más descriptivos y poner los comentarios en las funciones.
Thomas Ahle

Yuck Tomaré la solución de 1 línea que se lee de manera concisa con la evaluación de cortocircuito (que ha estado en idiomas durante más de 20 años y es bien conocida) sobre este desastre cualquier día. Creo que ambos podemos estar de acuerdo en que estamos felices de no trabajar juntos.
M2tM

8

Haga que sus funciones de ejecución arrojen una excepción si fallan en lugar de devolver false. Entonces su código de llamada podría verse así:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

Por supuesto, supongo que en su ejemplo original, el paso de ejecución solo devolvería falso en el caso de que ocurra un error dentro del paso.


3
El uso de la excepción para controlar el flujo a menudo se considera una mala práctica y un código maloliente
usuario902383

8

Ya hay muchas buenas respuestas, pero la mayoría de ellas parecen compensar algunas (ciertamente muy pocas) de la flexibilidad. Un enfoque común que no requiere esta compensación es agregar una variable de estado / continuación . El precio es, por supuesto, un valor adicional para realizar un seguimiento de:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

¿Por qué ok &= conditionX;y no simplemente ok = conditionX;?
ABCplus

@ user3253359 En muchos casos, sí, puedes hacerlo. Esta es una demostración conceptual; en el código de trabajo, intentaríamos simplificarlo tanto como sea posible
2014

+1 Una de las pocas respuestas limpias y mantenibles que funciona en c , como se estipula en la pregunta.
ClickRick

6

En C ++ (la pregunta está etiquetada con C y C ++), si no puede cambiar las funciones para usar excepciones, aún puede usar el mecanismo de excepción si escribe una pequeña función auxiliar como

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Entonces su código podría leer lo siguiente:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Si te gusta la sintaxis elegante, puedes hacer que funcione a través de un reparto explícito:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Entonces puedes escribir tu código como

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Refactorizar las comprobaciones de valor en excepciones no es necesariamente un buen camino a seguir, hay considerables excepciones de desenrollado de gastos generales.
John Wu

44
-1 El uso de excepciones para el flujo normal en C ++ como este es una mala práctica de programación. En C ++, las excepciones deben reservarse para circunstancias excepcionales.
Jack Aidley

1
Del texto de la pregunta (énfasis por mí): "Las funciones executeStepX deben ejecutarse si y solo si el anterior tiene éxito " . En otras palabras, el valor de retorno se usa para indicar un fallo. Es decir, se trata de manejo de errores (y uno esperaría que las fallas sean excepcionales). El manejo de errores es exactamente para lo que se inventaron las excepciones.
celtschk

1
No En primer lugar, se crearon excepciones para permitir la propagación de errores , no el manejo de errores ; en segundo lugar, "Las funciones executeStepX deben ejecutarse si y solo si las anteriores tienen éxito". no significa que el valor booleano falso devuelto por la función anterior indique un caso obviamente excepcional / erróneo. Su declaración es, por lo tanto, no sequitur . El manejo de errores y la desinfección del flujo se pueden implementar de muchas maneras diferentes, las excepciones son una herramienta que permite la propagación de errores y el manejo de errores fuera de lugar , y se destacan en eso.

6

Si su código es tan simple como su ejemplo y su idioma admite evaluaciones de cortocircuito, puede intentar esto:

StepA() && StepB() && StepC() && StepD();
DoAlways();

Si pasa argumentos a sus funciones y obtiene otros resultados para que su código no se pueda escribir de la manera anterior, muchas de las otras respuestas se adaptarían mejor al problema.


De hecho, edité mi pregunta para explicar mejor el tema, pero fue rechazada para no invalidar la mayoría de las respuestas. : \
ABCplus 01 de

Soy un nuevo usuario en SO y un programador novato. 2 preguntas entonces: ¿existe el riesgo de que otra pregunta como la que usted dijo se marque como duplicada debido a ESTA pregunta? Otro punto es: ¿cómo podría un usuario / programador SO novato elegir la mejor respuesta entre todas (supongo que casi bueno ...)?
ABCplus 01 de

6

Para C ++ 11 y más allá, un buen enfoque podría ser implementar un sistema de salida de alcance similar al mecanismo de alcance (salida) de D.

Una posible forma de implementarlo es usando C ++ 11 lambdas y algunas macros auxiliares:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

Esto le permitirá regresar temprano de la función y garantizar que cualquier código de limpieza que defina siempre se ejecute al salir del ámbito:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Las macros son realmente solo decoración. MakeScopeExit()Se puede usar directamente.


No hay necesidad de macros para que esto funcione. Y [=]generalmente está mal para una lambda con alcance.
Yakk - Adam Nevraumont

Sí, las macros son solo para decoración y se pueden tirar. ¿Pero no diría que la captura por valor es el enfoque "genérico" más seguro?
glampert

1
no: si su lambda no persistirá más allá del alcance actual donde se crea el lambda, use [&]: es seguro y mínimamente sorprendente. Capture por valor solo cuando la lambda (o copias) pueda sobrevivir más tiempo que el alcance en el punto de declaración ...
Yakk - Adam Nevraumont

Sí, eso tiene sentido. Lo cambiaré ¡Gracias!
glampert

6

¿Por qué nadie da la solución más simple? :RE

Si todas sus funciones tienen la misma firma, puede hacerlo de esta manera (para lenguaje C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Para una solución limpia de C ++, debe crear una clase de interfaz que contenga un método de ejecución y envuelva sus pasos en objetos.
Entonces, la solución anterior se verá así:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

5

Suponiendo que no necesita variables de condición individuales, invirtiendo las pruebas y utilizando el else-falthrough como la ruta "ok" le permitiría obtener un conjunto más vertical de sentencias if / else:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

La omisión de la variable fallida hace que el código sea demasiado oscuro para la OMI.

Declarar las variables dentro está bien, no te preocupes por = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Esto es oscuro, pero compacto:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

Esto parece una máquina de estado, lo cual es útil porque puede implementarlo fácilmente con un patrón de estado .

En Java se vería así:

interface StepState{
public StepState performStep();
}

Una implementación funcionaría de la siguiente manera:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

Y así. Entonces puede sustituir la condición big if con:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

Como Rommik mencionó, podría aplicar un patrón de diseño para esto, pero usaría el patrón Decorador en lugar de la Estrategia, ya que desea encadenar llamadas. Si el código es simple, entonces iría con una de las respuestas bien estructuradas para evitar el anidamiento. Sin embargo, si es complejo o requiere un encadenamiento dinámico, entonces el patrón Decorador es una buena opción. Aquí hay un diagrama de clase yUML :

diagrama de clase yUML

Aquí hay un ejemplo del programa LinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

El mejor libro para leer sobre patrones de diseño, en mi humilde opinión, es Head First Design Patterns .


¿Cuál es el beneficio de esto sobre algo como la respuesta de Jefffrey?
Dason

Mucho más resistencia al cambio, cuando los requisitos cambian, este enfoque es más simple de administrar sin mucho conocimiento de dominio. Especialmente cuando considera cuán profundo y largo pueden llegar a ser algunas secciones de ifs anidados. Todo puede volverse muy frágil y, por lo tanto, un alto riesgo para trabajar. No me malinterpreten, algunos escenarios de optimización pueden resultar en que eliminen esto y vuelvan a ifs, pero el 99% del tiempo está bien. Pero el punto es que cuando llegas a ese nivel no te importa la mantenibilidad, necesitas el rendimiento.
John Nicholas

4

Varias respuestas insinuaron un patrón que vi y usé muchas veces, especialmente en la programación de redes. En las pilas de red a menudo hay una larga secuencia de solicitudes, cualquiera de las cuales puede fallar y detendrá el proceso.

El patrón común era usar do { } while (false);

Usé una macro para while(false)hacerlo. do { } once;El patrón común era:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

Este patrón era relativamente fácil de leer, y permitía el uso de objetos que destruirían adecuadamente y también evitaba múltiples retornos, lo que facilitaba un poco los pasos y la depuración.


4

Para mejorar la respuesta de C ++ 11 de Mathieu y evitar el costo de tiempo de ejecución incurrido por el uso de std::function, sugeriría usar lo siguiente

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Esta clase de plantilla simple aceptará cualquier functor que pueda llamarse sin ningún parámetro, y lo hace sin ninguna asignación de memoria dinámica y, por lo tanto, se ajusta mejor al objetivo de abstracción de C ++ sin sobrecarga innecesaria. La plantilla de función adicional está ahí para simplificar el uso mediante la deducción de parámetros de plantilla (que no está disponible para los parámetros de plantilla de clase)

Ejemplo de uso:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Al igual que la respuesta de Mathieu, esta solución es totalmente segura para excepciones y executeThisFunctionInAnyCasese llamará en todos los casos. En caso de que executeThisFunctionInAnyCasese arroje, los destructores se marcan implícitamente noexcepty, por lo tanto std::terminate, se emitirá una llamada a en lugar de provocar una excepción durante el desenrollado de la pila.


+1 Estaba buscando esta respuesta para no tener que publicarla. Debe perfeccionar el avance functoren deferred'd constructor, no es necesario forzar a move.
Yakk - Adam Nevraumont

@Yakk cambió el constructor a un constructor de reenvío
Joe

3

Parece que quieres hacer todas tus llamadas desde un solo bloque. Como otros lo han propuesto, debe usar un whilebucle y dejar de usar breako una nueva función que puede dejar conreturn (puede ser más limpia).

Yo personalmente desterro goto , incluso para la función de salida. Son más difíciles de detectar al depurar.

Una alternativa elegante que debería funcionar para su flujo de trabajo es construir una matriz de funciones e iterar sobre esta.

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();

Afortunadamente, el depurador los detecta por ti. Si está depurando y no está avanzando a través del código, lo está haciendo mal.
Cody Gray

No entiendo lo que quieres decir, ¿y por qué no pude usar un solo paso?
AxFab

3

Porque también tienes [... bloque de código ...] entre ejecuciones, supongo que tiene asignación de memoria o inicializaciones de objetos. De esta manera, debe preocuparse por limpiar todo lo que ya ha inicializado en la salida, y también limpiarlo si encuentra algún problema y alguna de las funciones devuelve falso.

En este caso, lo mejor que tuve en mi experiencia (cuando trabajé con CryptoAPI) fue crear clases pequeñas, en el constructor inicializas tus datos, en el destructor no los inicializas. Cada clase de función siguiente debe ser hija de la clase de función anterior. Si algo salió mal, lanza una excepción.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

Y luego en su código solo necesita llamar:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Supongo que es la mejor solución si cada llamada de ConditionX inicializa algo, asigna memoria, etc. Lo mejor es asegurarse de que todo se limpie.


3

Una forma interesante es trabajar con excepciones.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Si escribe dicho código, de alguna manera va en la dirección equivocada. No lo veré como "el problema" tener ese código, sino tener una "arquitectura" tan desordenada.

Consejo: discuta esos casos con un desarrollador experimentado en el que confíe ;-)


Creo que esta idea no puede reemplazar cada cadena if. De todos modos, en muchos casos, ¡este es un muy buen enfoque!
El

3

Otro enfoque: do - whilebucle, a pesar de que se mencionó antes, no había ningún ejemplo que mostrara cómo se ve:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Bueno, ya hay una respuesta con el whilebucle, pero el do - whilebucle no verifica de forma redundante si es verdadero (al principio), sino al final xD (sin embargo, esto se puede omitir)).


Hola Zaffy: esta respuesta tiene una explicación masiva del enfoque do {} while (false). stackoverflow.com/a/24588605/294884 Otras dos respuestas también lo mencionaron.
Fattie

¡Es una pregunta fascinante si es más elegante usar CONTINUAR o DESCONECTAR en esta situación!
Fattie

Hola @JoeBlow, vi todas las respuestas ... solo quería mostrar en lugar de hablar sobre eso :)
Zaffy

Mi primera respuesta aquí dije "Nadie ha mencionado ESTO ..." e inmediatamente alguien amablemente señaló que era la segunda respuesta principal :)
Fattie

@ JoeBlow Eh, tienes razón. Trataré de arreglar eso. Siento que ... xD Gracias de todos modos, la próxima vez pagaré un poco más de
atención
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.