Quali sono alcuni modi migliori per evitare il do-while (0); hack in C ++?


233

Quando il flusso di codice è così:

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

In genere ho visto questo lavoro in giro per evitare il flusso di codice disordinato sopra:

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

Quali sono alcuni modi migliori per evitare questa soluzione alternativa / hacking in modo che diventi un codice di livello superiore (a livello di settore)?

Tutti i suggerimenti che sono fuori dalla scatola sono i benvenuti!


38
RAII e generare eccezioni.
ta.speot.is

135
Per me questo sembra un buon momento da usare goto, ma sono sicuro che qualcuno mi segnerà per averlo suggerito, quindi non scrivo una risposta in tal senso. Avere una LUNGA do ... while(0);sembra la cosa sbagliata.
Mats Petersson,

42
@dasblinkenlight: Sì, davvero. Se hai intenzione di usarlo goto, sii onesto e fallo all'aperto, non nasconderlo usando breakedo ... while
Mats Petersson,

44
@MatsPetersson: fagli una risposta, ti darò +1 per il tuo gotosforzo di riabilitazione. :)
Wilx,

27
@ ta.speot.is: "RAII e genera eccezioni". In questo modo, emulerai il controllo del flusso con eccezioni. Vale a dire che è un po 'come usare hardware costoso e spigoloso come martello o fermacarte. Puoi farlo, ma per me sembra decisamente un pessimo gusto.
SigTerm,

Risposte:


309

È considerata pratica accettabile isolare queste decisioni in una funzione e usare returns invece dibreak s. Mentre tutti questi controlli corrispondono allo stesso livello di astrazione della funzione, è un approccio abbastanza logico.

Per esempio:

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

22
@MatsPetersson: "Isolare in funzione" significa refactoring in una nuova funzione che esegue solo i test.
MSalters,

35
+1. Questa è anche una buona risposta. In C ++ 11, la funzione isolata può essere una lambda, in quanto può anche catturare le variabili locali, quindi rende le cose facili!
Nawaz,

4
@deworde: a seconda della situazione, questa soluzione potrebbe diventare molto più lunga e meno leggibile di goto. Poiché il C ++, sfortunatamente, non consente la definizione di funzioni locali, dovrai spostare quella funzione (quella da cui tornerai) da qualche altra parte, il che riduce la leggibilità. Potrebbe finire con dozzine di parametri, quindi, poiché avere dozzine di parametri è una cosa negativa, deciderai di includerli in Struct, che creerà un nuovo tipo di dati. Troppa battitura per una situazione semplice.
SigTerm,

24
@Damon semmai, returnè più pulito perché qualsiasi lettore è immediatamente consapevole che funziona correttamente e cosa fa. Con gotobisogna guardarsi intorno per vedere che cosa è per, e per essere sicuri che non sono stati commessi errori. Il travestimento è il vantaggio.
R. Martinho Fernandes,

11
@SigTerm: a volte il codice dovrebbe essere spostato in una funzione separata solo per mantenere la dimensione di ciascuna funzione fino a una dimensione facile da ragionare. Se non si desidera pagare per una chiamata di funzione, contrassegnarla forceinline.
Ben Voigt,

257

Ci sono momenti in cui l'utilizzo gotoè in realtà la risposta GIUSTA - almeno per coloro che non sono cresciuti nella convinzione religiosa che "goto non può mai essere la risposta, non importa quale sia la domanda" - e questo è uno di quei casi.

Questo codice utilizza l'hacking di do { ... } while(0);al solo scopo di vestire un gotoas a break. Se hai intenzione di usaregoto , sii aperto a riguardo. Non ha senso rendere il codice più difficile da leggere.

Una situazione particolare è proprio quando hai un sacco di codice con condizioni abbastanza complesse:

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

  }
  finish up. 
}

Può effettivamente rendere più CHIARO che il codice è corretto non avendo una logica così complessa.

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. 
}

Modifica: Per chiarire, non sto assolutamente proponendo l'uso gotocome soluzione generale. Ma ci sono casi in cuigoto c'è una soluzione migliore rispetto ad altre soluzioni.

Immagina, ad esempio, che stiamo raccogliendo alcuni dati e che le diverse condizioni da testare siano una sorta di "questa è la fine dei dati raccolti", che dipende da una sorta di marcatori "continua / fine" che variano a seconda di dove sei nel flusso di dati.

Ora, quando abbiamo finito, dobbiamo salvare i dati in un file.

E sì, ci sono spesso altre soluzioni che possono fornire una soluzione ragionevole, ma non sempre.


76
Disaccordo. gotopuò avere un posto, ma goto cleanupno. La pulizia viene eseguita con RAII.
Salteri il

14
@MSalters: ciò presuppone che la pulizia comporti qualcosa che può essere risolto con RAII. Forse avrei dovuto dire "errore di problema" o alcuni di questi invece.
Mats Petersson,

25
Quindi, continua a ricevere voti negativi su questo, presumibilmente da quelli della credenza religiosa di gotonon è mai la risposta giusta. Gradirei se ci fosse un commento ...
Mats Petersson,

19
le persone odiano gotoperché devi pensare di usarlo / per capire un programma che lo usa ... I microprocessori, d'altra parte, sono costruiti su ... jumpse conditional jumpsquindi il problema è con alcune persone, non con la logica o qualcos'altro.
woliveirajr,

17
+1 per l'uso appropriato di goto. RAII non è la soluzione corretta se si prevedono errori (non eccezionali), in quanto si tratterebbe di un abuso di eccezioni.
Joe,

82

È possibile utilizzare un semplice modello di continuazione con una boolvariabile:

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

Questa catena di esecuzione si interromperà non appena checkNritorna a false. Non check...()verranno eseguite ulteriori chiamate a causa di cortocircuiti &&dell'operatore. Inoltre, i compilatori di ottimizzazione sono abbastanza intelligente da riconoscere che l'impostazione goOndi falseuna strada a senso unico, e inserire i dispersi goto endper voi. Di conseguenza, l'esecuzione del codice sopra sarebbe identica a quella di un do/ while(0), solo senza un colpo doloroso alla sua leggibilità.


30
Le assegnazioni all'interno delle ifcondizioni sembrano molto sospette.
Mikhail,

90
@Mikhail Solo ad un occhio non allenato.
dasblinkenlight,

20
Ho usato questa tecnica prima e mi ha sempre infastidito il fatto che ho pensato che il compilatore avrebbe dovuto generare codice per verificare a ciascuno, ifqualunque cosa goOnfosse anche se una delle prime falliva (al contrario di saltare / scoppiare) ... ma ho appena eseguito un test e VS2012 almeno era abbastanza intelligente da cortocircuitare tutto dopo il primo falso comunque. Lo userò più spesso. Nota: se si utilizza goOn &= checkN()quindi checkN()sarà sempre eseguito, anche se goOnera falseall'inizio del if(vale a dire, non farlo).
segna il

11
@Nawaz: se hai una mente inesperta che apporta modifiche arbitrarie su una base di codice, hai un problema molto più grande dei semplici incarichi all'interno di ifs.
Idelico,

6
@sisharp L'eleganza è così negli occhi di chi guarda! Non riesco a capire come nel mondo un uso improprio di un costrutto loop possa in qualche modo essere percepito come "elegante", ma forse sono solo io.
dasblinkenlight,

38
  1. Prova a estrarre il codice in una funzione separata (o forse più di una). Quindi tornare dalla funzione se il controllo fallisce.

  2. Se è troppo strettamente accoppiato con il codice circostante per farlo, e non riesci a trovare un modo per ridurre l'accoppiamento, guarda il codice dopo questo blocco. Presumibilmente, pulisce alcune risorse utilizzate dalla funzione. Prova a gestire queste risorse usando un oggetto RAII ; quindi sostituisci ogni schivata breakcon return(o throw, se è più appropriato) e lascia che il distruttore dell'oggetto pulisca per te.

  3. Se il flusso del programma è (necessariamente) così debole che hai davvero bisogno di un goto, allora usa quello piuttosto che dargli uno strano travestimento.

  4. Se hai regole di codifica che ciecamente vietano gotoe davvero non puoi semplificare il flusso del programma, probabilmente dovrai mascherarlo con il tuo dohack.


4
Sottolineo umilmente che RAII, sebbene utile, non è un proiettile d'argento. Quando ti ritrovi a scrivere una classe convert-goto-in-RAII che non ha altro uso, penso sicuramente che ti serviresti meglio usando il linguaggio "goto-end-of-the-world" già menzionato.
idoby,

1
@busy_wait: In effetti, RAII non può risolvere tutto; ecco perché la mia risposta non si ferma al secondo punto, ma continua a suggerire gotose questa è davvero un'opzione migliore.
Mike Seymour,

3
Sono d'accordo, ma penso che sia una cattiva idea scrivere classi di conversione goto-RAII e penso che dovrebbe essere dichiarato esplicitamente.
idoby,

@idoby che ne dici di scrivere una classe di template RAII e di crearne un'istanza con qualsiasi pulizia monouso di cui hai bisogno in una lambda
Caleth

@Caleth mi suona come se stessi reinventando dottori / dottori?
idoby

37

TLDR : RAII , codice transazionale (impostare risultati o restituire elementi solo quando è già calcolato) ed eccezioni.

Risposta lunga:

In C , la migliore pratica per questo tipo di codice è quella di aggiungere un'etichetta EXIT / CLEANUP / altra nel codice, in cui avviene la pulizia delle risorse locali e viene restituito un codice di errore (se presente). Questa è la migliore pratica perché divide naturalmente il codice in inizializzazione, calcolo, commit e return:

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;
}

In C, nella maggior parte dei basi di codice, il if(error_ok != ...e il gotocodice è di solito nascosto dietro alcune macro di convenienza ( RET(computation_result), ENSURE_SUCCESS(computation_result, return_code), ecc).

C ++ offre strumenti extra rispetto a C :

  • La funzionalità del blocco di pulizia può essere implementata come RAII, il che significa che non è più necessario l'intero cleanupblocco e consente al codice client di aggiungere dichiarazioni di restituzione anticipate.

  • Lanci ogni volta che non puoi continuare, trasformando tutte le if(error_ok != ...chiamate in diretta.

Codice C ++ equivalente:

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

Questa è la migliore pratica perché:

  • È esplicito (ovvero, mentre la gestione degli errori non è esplicita, il flusso principale dell'algoritmo è)

  • È semplice scrivere il codice client

  • È minimo

  • È semplice

  • Non ha costrutti di codice ripetitivi

  • Non utilizza macro

  • Non usa do { ... } while(0)costrutti strani

  • È riutilizzabile con il minimo sforzo (ovvero, se voglio copiare la chiamata in computation2();un'altra funzione, non devo assicurarmi di aggiungere un do { ... } while(0)nel nuovo codice, né #defineuna macro wrapper e un'etichetta di pulizia, né qualunque altra cosa).


+1. Questo è quello che cerco di usare, usando RAII o qualcosa del genere. shared_ptrcon un deleter personalizzato puoi fare molte cose. Ancora più facile con lambdas in C ++ 11.
Macke,

Vero, ma se finisci per usare shared_ptr per cose che non sono puntatori, considera almeno la battitura a macchina: namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };in questo caso (con l' make_handleimpostazione del tipo di deleter corretto sulla costruzione), il nome del tipo non suggerisce più che sia un puntatore .
utnapistim,

21

Sto aggiungendo una risposta per completezza. Numerose altre risposte hanno sottolineato che il blocco delle condizioni di grandi dimensioni potrebbe essere suddiviso in una funzione separata. Ma, come è stato anche sottolineato più volte, questo approccio separa il codice condizionale dal contesto originale. Questo è uno dei motivi per cui i lambda sono stati aggiunti alla lingua in C ++ 11. L'uso di lambda è stato suggerito da altri ma non è stato fornito alcun campione esplicito. Ne ho inserito uno in questa risposta. Ciò che mi colpisce è che sembra molto simile do { } while(0)all'approccio in molti modi - e forse ciò significa che è ancora gotosotto mentite spoglie ...

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

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

7
Per me questo hack sembra peggio del do ... mentre hack.
Michael,

18

Certamente non la risposta, ma una risposta (per completezza)

Invece di :

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

Puoi scrivere:

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

Questo è ancora un goto sotto mentite spoglie, ma almeno non è più un ciclo. Ciò significa che non dovrete controllare con molta attenzione che non ci sia alcuno di continuare nascosti da qualche parte nel blocco.

Il costrutto è anche abbastanza semplice da sperare che il compilatore lo ottimizzi via.

Come suggerito da @jamesdlin, puoi persino nasconderlo dietro una macro come

#define BLOC switch(0) case 0:

E usalo come

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

Ciò è possibile perché la sintassi del linguaggio C prevede un'istruzione dopo uno switch, non un blocco tra parentesi e puoi inserire un'etichetta case prima di tale istruzione. Fino ad ora non ho visto il punto di permetterlo, ma in questo caso particolare è utile nascondere l'interruttore dietro una bella macro.


2
Ooh, intelligente. Potresti persino nasconderlo dietro una macro define BLOCK switch (0) case 0:e usarlo come BLOCK { ... break; }.
jamesdlin,

@jamesdlin: non mi è mai venuto in mente che potesse essere utile mettere una custodia davanti alla staffa di apertura di un interruttore. Ma è davvero consentito da C e in questo caso è utile scrivere una bella macro.
Kriss,

15

Consiglierei un approccio simile alla risposta di Mats meno l'inutile goto. Metti solo la logica condizionale nella funzione. Qualsiasi codice che viene sempre eseguito dovrebbe andare prima o dopo che la funzione è stata invocata nel chiamante:

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
Se devi acquisire un'altra risorsa nel mezzo di func, devi fattorizzare un'altra funzione (secondo il tuo modello). Se tutte queste funzioni isolate hanno bisogno degli stessi dati, finirai semplicemente per copiare gli stessi argomenti dello stack più o più volte, o deciderai di allocare i tuoi argomenti sull'heap e passare un puntatore in giro, non sfruttando la più basilare delle funzionalità del linguaggio ( argomenti di funzione). In breve, non credo che questa soluzione si ridimensiona nel caso peggiore in cui ogni condizione viene verificata dopo aver acquisito nuove risorse che devono essere ripulite. Nota comunque il commento di Nawaz su lambdas.
TNE

2
Non ricordo che l'OP abbia detto nulla sull'acquisizione di risorse nel mezzo del suo blocco di codice, ma lo accetto come un requisito fattibile. In tal caso, cosa c'è di sbagliato nel dichiarare la risorsa in pila ovunque func()e consentire al suo distruttore di gestire le risorse di liberazione? Se qualcosa al di fuori di esso func()necessita dell'accesso alla stessa risorsa, deve essere dichiarato sull'heap prima di essere chiamato func()da un gestore delle risorse adeguato.
Dan Bechard,

12

Il flusso di codice stesso è già un odore di codice che sta succedendo molto nella funzione. Se non esiste una soluzione diretta a ciò (la funzione è una funzione di controllo generale), utilizzare RAII in modo da poter tornare invece di saltare a una sezione finale della funzione potrebbe essere migliore.


11

Se non è necessario introdurre variabili locali durante l'esecuzione, è spesso possibile appiattire questo:

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

// edit 
doThingsAtEndAndReportErrorStatus()

2
Ma poi ogni condizione dovrebbe includere la precedente, che non è solo brutta ma potenzialmente dannosa per le prestazioni. Meglio saltare quei controlli e ripulire immediatamente non appena sappiamo che "non va bene".
TNE

È vero, tuttavia questo approccio ha dei vantaggi se alla fine ci sono delle cose da fare, quindi non vuoi tornare presto (vedi modifica). Se combini con l'approccio di Denise di usare bool nelle condizioni, il colpo di prestazione sarà trascurabile a meno che questo non sia in un circuito molto stretto.
the_mandrill

10

Simile alla risposta di dasblinkenlight, ma evita l'assegnazione all'interno ifche potrebbe essere "riparata" da un revisore del codice:

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

...

Uso questo modello quando i risultati di un passaggio devono essere controllati prima del passaggio successivo, che differisce da una situazione in cui tutti i controlli potrebbero essere eseguiti in anticipo con un if( check1() && check2()...modello di tipo grande .


Notare che check1 () potrebbe davvero essere PerformStep1 () che restituisce un codice risultato del passaggio. Ciò ridurrebbe la complessità della funzione del flusso di processo.
Denise Skidmore,

10

Usa le eccezioni. Il codice apparirà molto più pulito (e sono state create eccezioni esattamente per la gestione degli errori nel flusso di esecuzione di un programma). Per ripulire le risorse (descrittori di file, connessioni al database, ecc.), Leggi l'articolo Perché C ++ non fornisce un costrutto "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
Veramente? Innanzitutto, non vedo alcun miglioramento nella leggibilità, lì. NON USARE ECCEZIONI PER CONTROLLARE IL FLUSSO DEL PROGRAMMA. Questo NON è il loro scopo. Inoltre, le eccezioni comportano significative penalità di prestazione. Il caso d'uso corretto per un'eccezione è quando è presente una condizione che NON PUOI FARE NULLA, ad esempio tentare di aprire un file che è già bloccato esclusivamente da un altro processo o una connessione di rete non riesce o una chiamata a un database fallisce oppure un chiamante passa un parametro non valido a una procedura. Questo genere di cose. Ma NON utilizzare le eccezioni per controllare il flusso del programma.
Craig,

3
Voglio dire, non essere un secchione, ma vai a quell'articolo di Stroustrup a cui hai fatto riferimento e cerca "Per cosa non dovrei usare le eccezioni?" sezione. Tra le altre cose, afferma: "In particolare, lanciare non è semplicemente un modo alternativo di restituire un valore da una funzione (simile a return). In questo modo sarà lento e confonderà la maggior parte dei programmatori C ++ abituati a vedere le eccezioni usate solo per errore gestione. Allo stesso modo, lanciare non è un buon modo per uscire da un ciclo. "
Craig,

3
@Craig Tutto ciò che hai indicato è giusto, ma stai assumendo che il programma di esempio possa continuare dopo che una check()condizione fallisce, e questo è sicuramente il TUO presupposto, non c'è contesto nel campione. Supponendo che il programma non possa continuare, utilizzare le eccezioni è la strada da percorrere.
Cartucho,

2
Bene, abbastanza vero se questo è in realtà il contesto. Ma, cosa divertente riguardo alle ipotesi ... ;-)
Craig,

10

Per me do{...}while(0)va bene. Se non vuoi vedere ildo{...}while(0) , puoi definire parole chiave alternative per loro.

Esempio:

//--------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

Penso che il compilatore rimuoverà la while(0)condizione non necessaria indo{...}while(0) versione binaria e convertirà le interruzioni in salto incondizionato. Puoi verificarne la versione in linguaggio assembly per essere sicuro.

L'utilizzo gotoproduce anche un codice più pulito ed è semplice con la logica condition-then-jump. Puoi fare quanto segue:

{
   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.

Si noti che l'etichetta viene posizionata dopo la chiusura }. Questo è l'evitamento di un possibile problema gotoche consiste nel posizionare accidentalmente un codice in mezzo perché non hai visto l'etichetta. Ora è come do{...}while(0)senza codice condizione.

Per rendere questo codice più pulito e comprensibile, puoi farlo:

//--------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 questo, puoi fare blocchi nidificati e specificare dove vuoi uscire / saltare.

//--------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);

   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)

Il codice spaghetti non è colpa di goto, è colpa del programmatore. Puoi ancora produrre il codice spaghetti senza usare goto.


10
Sceglierei di gotoestendere la sintassi del linguaggio usando il preprocessore un milione di volte.
Christian,

2
"Per rendere questo codice più pulito e comprensibile, puoi [usare LOADS_OF_WEIRD_MACROS]" : non calcola.
underscore_d,

8

Questo è un problema ben noto e risolto dal punto di vista funzionale della programmazione - forse la monade.

In risposta al commento che ho ricevuto di seguito ho modificato la mia introduzione qui: Puoi trovare tutti i dettagli sull'implementazione delle monadi C ++ in vari luoghi che ti permetteranno di ottenere ciò che suggerisce Rotsor. Ci vuole un po 'per ingannare le monadi, quindi suggerirò qui un rapido meccanismo simile a una monade "povero" per il quale non è necessario sapere altro che boost :: opzionale.

Imposta i tuoi passi di calcolo come segue:

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

Ogni passaggio computazionale può ovviamente fare qualcosa come return boost::nonese l'opzione opzionale che gli è stata fornita è vuota. Quindi per esempio:

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.
}

Quindi incatenali insieme:

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
}

La cosa bella di questo è che puoi scrivere unit test chiaramente definiti per ogni passaggio computazionale. Anche l'invocazione si legge come un semplice inglese (come di solito accade con lo stile funzionale).

Se non ti interessa l'immutabilità ed è più conveniente restituire lo stesso oggetto ogni volta che potresti trovare qualche variazione usando shared_ptr o simili.


3
Questo codice ha una proprietà indesiderata di forzare ciascuna delle singole funzioni a gestire il fallimento della funzione precedente, quindi non utilizzando correttamente il linguaggio Monad (dove gli effetti monadici, in questo caso il fallimento, dovrebbero essere gestiti implicitamente). Per fare ciò è necessario disporre optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);invece e utilizzare l'operazione di composizione monadica ('bind') anziché l'applicazione della funzione.
Rotsor,

Grazie. Hai ragione: questo è il modo di farlo correttamente. Non volevo scrivere troppo nella mia risposta per spiegarlo (da qui il termine "uomo povero" che doveva suggerire che non avrei seguito tutto il maiale qui).
Benedetto

7

Che ne dici di spostare le istruzioni if ​​in una funzione extra che produce un risultato numerico o enumico?

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;
   }
}

Questo è carino quando possibile, ma molto meno generale della domanda posta qui. Ogni condizione potrebbe dipendere dal codice eseguito dopo l'ultimo test di debranching.
Kriss,

Il problema qui è la causa e le conseguenze separate. Vale a dire codice separato che fa riferimento allo stesso numero di condizione e questo può essere una fonte di ulteriori bug.
Riga,

@kriss: Beh, la funzione ConditionCode () potrebbe essere regolata per occuparsene. Il punto chiave di quella funzione è che è possibile utilizzare return <result> per un'uscita pulita non appena viene calcolata una condizione finale; E questo è ciò che fornisce chiarezza strutturale qui.
karx11erx,

@Riga: Imo queste sono obiezioni completamente accademiche. Trovo che C ++ diventi più complesso, criptico e illeggibile con ogni nuova versione. Non riesco a vedere un problema con una piccola funzione di supporto che valuti condizioni complesse in un modo ben strutturato per rendere più leggibile la funzione target proprio sotto.
karx11erx,

1
@ karx11erx le mie obiezioni sono pratiche e basate sulla mia esperienza. Questo modello è erroneamente non correlato a C ++ 11 o qualsiasi altro linguaggio. Se hai difficoltà con i costrutti del linguaggio che ti permettono di scrivere una buona architettura che non è un problema linguistico.
Riga,

5

Qualcosa del genere forse

#define EVER ;;

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

o utilizzare le eccezioni

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

Usando le eccezioni puoi anche passare i dati.


10
Per favore, non fare cose intelligenti come la tua definizione di sempre, di solito rendono il codice più difficile da leggere per gli altri sviluppatori. Ho visto qualcuno definire Case come break; case in un file header, e l'ho usato in uno switch in un file cpp, facendo meravigliare gli altri per ore perché lo switch si interrompe tra le istruzioni Case. Grrr ...
Michael,

5
E quando dai un nome alle macro, dovresti farle apparire come macro (ovvero, tutte in maiuscolo). Altrimenti qualcuno a cui capita di nominare una variabile / funzione / tipo / ecc. il nome eversarà molto infelice ...
jamesdlin il

5

Non mi piace particolarmente usare breakoreturn in tal caso. Dato che normalmente quando ci troviamo di fronte a una situazione del genere, di solito è un metodo relativamente lungo.

Se abbiamo più punti di uscita, ciò può causare difficoltà quando vogliamo sapere cosa causerà l'esecuzione di una certa logica: normalmente continuiamo a salire su blocchi che racchiudono quel pezzo di logica, e i criteri di quei blocchi che racchiudono ci dicono il situazione:

Per esempio,

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

Osservando i blocchi racchiusi, è facile scoprire che ciò myLogic()accade solo quando conditionA and conditionB and conditionCè vero.

Diventa molto meno visibile quando ci sono ritorni precoci:

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

Non possiamo più risalire da myLogic(), guardando il blocco che racchiude per capire la condizione.

Ci sono diverse soluzioni alternative che ho usato. Eccone uno:

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

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

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

(Ovviamente è gradito usare la stessa variabile per sostituire tutto isA isB isC .)

Tale approccio fornirà almeno al lettore di codice, che myLogic()viene eseguito quando isB && conditionC. Al lettore viene dato un suggerimento che deve cercare ulteriormente ciò che farà sì che isB sia vero.


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++;
}

Che ne dici di quello?


l'if è obsoleto, giusto return condition;, altrimenti penso che sia ben mantenibile.
SpaceTrucker,

2

Un altro modello utile se sono necessari diversi passaggi di pulizia a seconda di dove si trova l'errore:

    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

Non sono un programmatore C ++ , quindi non scriverò alcun codice qui, ma finora nessuno ha menzionato una soluzione orientata agli oggetti. Quindi ecco la mia ipotesi su questo:

Avere un'interfaccia generica che fornisce un metodo per valutare una singola condizione. Ora puoi utilizzare un elenco di implementazioni di tali condizioni nel tuo oggetto contenente il metodo in questione. Si scorre l'elenco e si valuta ogni condizione, possibilmente esplodendo in anticipo se si fallisce.

La cosa buona è che tale design si attiene molto bene al principio aperto / chiuso , perché è possibile aggiungere facilmente nuove condizioni durante l'inizializzazione dell'oggetto contenente il metodo in questione. È anche possibile aggiungere un secondo metodo all'interfaccia con il metodo per la valutazione delle condizioni restituendo una descrizione della condizione. Questo può essere usato per sistemi di auto-documentazione.

Il rovescio della medaglia, tuttavia, è che c'è un leggero sovraccarico in più a causa dell'uso di più oggetti e dell'iterazione sull'elenco.


Potresti aggiungere un esempio in un'altra lingua? Penso che questa domanda si applichi a molte lingue anche se è stata posta specificamente sul C ++.
Denise Skidmore,

1

Questo è il modo in cui lo faccio.

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

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

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

1

Innanzitutto, un breve esempio per mostrare perché gotonon è una buona soluzione per C ++:

struct Bar {
    Bar();
};

extern bool check();

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

    Bar x;

    out:
}

Prova a compilarlo in un file oggetto e guarda cosa succede. Quindi prova l'equivalente do+ break+while(0) .

Era un lato. Segue il punto principale.

Quei piccoli pezzi di codice spesso richiedono un qualche tipo di pulizia se l'intera funzione fallisce. Quelle pulizie di solito vogliono avvenire nell'ordine opposto rispetto ai pezzi stessi, mentre "srotoli" il calcolo parzialmente finito.

Un'opzione per ottenere queste semantiche è RAII ; vedi la risposta di @utnapistim. C ++ garantisce che i distruttori automatici funzionino nell'ordine opposto rispetto ai costruttori, il che fornisce naturalmente un "svolgersi".

Ma ciò richiede molte classi RAII. A volte un'opzione più semplice è solo per usare lo stack:

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;
}

...e così via. Questo è facile da controllare, poiché mette il codice "annulla" accanto al codice "do". Il controllo semplice è buono. Rende inoltre molto chiaro il flusso di controllo. È un modello utile anche per C.

Può richiedere che le calcfunzioni prendano molti argomenti, ma questo di solito non è un problema se le tue classi / strutture hanno una buona coesione. (Cioè, le cose che appartengono insieme vivono in un singolo oggetto, quindi queste funzioni possono prendere puntatori o riferimenti a un piccolo numero di oggetti e fare ancora molto lavoro utile.)


Molto facile controllare il percorso di pulizia, ma forse non è così facile tracciare il percorso d'oro. Ma nel complesso, penso che qualcosa del genere incoraggi un modello di pulizia coerente.
Denise Skidmore,

0

Se il codice ha un blocco lungo di istruzioni if..else if..else, puoi provare a riscrivere l'intero blocco con l'aiuto di Functorso function pointers. Potrebbe non essere la soluzione giusta sempre, ma abbastanza spesso lo è.

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


In linea di principio, ciò è possibile (e non male), ma con oggetti funzione espliciti o punti intermedi interrompe il flusso del codice in modo troppo forte. OTOH, qui equivalente, l'uso di lambda o normali funzioni nominate è una buona pratica, efficiente e legge bene.
leftaroundabout

0

Sono sorpreso dal numero di risposte diverse presentate qui. Ma, infine, nel codice che devo modificare (cioè rimuovere questo do-while(0)hack o altro), ho fatto qualcosa di diverso da una qualsiasi delle risposte menzionate qui e sono confuso perché nessuno lo ha pensato. Ecco cosa ho fatto:

Codice iniziale:

do {

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

finishingUpStuff.

Adesso:

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

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

Quindi, ciò che è stato fatto qui è che le cose di finitura sono state isolate in una funzione e le cose sono diventate improvvisamente così semplici e pulite!

Ho pensato che valesse la pena menzionare questa soluzione, quindi fornirla qui.


0

Consolidalo in una sola ifistruzione:

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

Questo è il modello utilizzato in lingue come Java in cui è stata rimossa la parola chiave goto.


2
Funziona solo se non devi fare nulla tra i test delle condizioni. (beh, suppongo che potresti nascondere le cose da fare all'interno di alcune chiamate di funzione fatte dai test delle condizioni, ma ciò potrebbe essere un po 'offuscato se lo facessi troppo)
Jeremy Friesner,

@JeremyFriesner In realtà, potresti effettivamente fare le cose intermedie come funzioni booleane separate che valutano sempre come true. La valutazione del cortocircuito garantirebbe che non si eseguano mai cose intermedie per le quali non sono stati superati tutti i test dei prerequisiti.
AJMansfield,

@AJMansfield sì, è quello a cui mi riferivo nella mia seconda frase ... ma non sono sicuro che sarebbe un miglioramento della qualità del codice.
Jeremy Friesner,

@JeremyFriesner Nulla ti impedisce di scrivere le condizioni in quanto (/*do other stuff*/, /*next condition*/), potresti persino formattarlo bene. Non aspettarti che alla gente piaccia. Ma onestamente, questo dimostra solo che è stato un errore per Java eliminare gradualmente questa gotoaffermazione ...
cmaster - reintegrare monica il

@JeremyFriesner Supponevo che fossero booleani. Se le funzioni dovessero essere eseguite all'interno di ciascuna condizione, allora esiste un modo migliore per gestirle.
Tyzoid,

0

Se si utilizza lo stesso gestore degli errori per tutti gli errori e ogni passaggio restituisce un valore booleano che indica l'esito positivo:

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

(Simile alla risposta del tyzoid, ma le condizioni sono le azioni e && impedisce che si verifichino ulteriori azioni dopo il primo fallimento.)


0

Perché il metodo di segnalazione non ha risposto, è usato da secoli.

//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
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.