Uso valido di goto per la gestione degli errori in C?


92

Questa domanda è in realtà il risultato di un'interessante discussione su programming.reddit.com qualche tempo fa. Fondamentalmente si riduce al seguente codice:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

L'uso di gotoqui sembra essere il modo migliore per procedere, risultando nel codice più pulito ed efficiente di tutte le possibilità, o almeno così mi sembra. Citando Steve McConnell in Code Complete :

Il goto è utile in una routine che alloca risorse, esegue operazioni su tali risorse e quindi rilascia le risorse. Con un goto, puoi pulire in una sezione del codice. Il goto riduce la probabilità che ti dimentichi di deallocare le risorse in ogni punto in cui rilevi un errore.

Un altro supporto per questo approccio viene dal libro Linux Device Drivers , in questa sezione .

Cosa ne pensi? Questo caso è un uso valido per gotoin C? Preferiresti altri metodi, che producono codice più contorto e / o meno efficiente, ma da evitare goto?



@Eli: Perché non rimuovi i tag e inserisci la funzione (cleanup_3 ();) tra parentesi di if?

@ Akito: cosa intendi? Potresti pubblicare il tuo suggerimento come risposta con un esempio di codice completo?
Eli Bendersky

@EliBendersky: guarda la mia risposta.

Una delle cose che odiavo di più da Visual Basic (inclusi VBS e VB6) era il on error goto errorsistema di gestione degli errori :)
Manu343726

Risposte:


61

FWIF, trovo che l'idioma di gestione degli errori che hai fornito nell'esempio della domanda sia più leggibile e più facile da capire rispetto a qualsiasi alternativa fornita nelle risposte finora. Sebbene gotosia una cattiva idea in generale, può essere utile per la gestione degli errori se eseguita in modo semplice e uniforme. In questa situazione, anche se è un goto, viene utilizzato in modo ben definito e più o meno strutturato.


1
Non può semplicemente rimuovere quei tag e inserire le funzioni direttamente in if? Non sarà più leggibile?

8
@StartupCrazy, so che ha anni, ma per motivi di validità dei post su questo sito farò notare che no non può. Se riceve un errore in goto error3 nel suo codice eseguirà clean up 1 2 e 3, nella tua soluzione suggerita eseguirà solo cleanup 3. Potrebbe annidare le cose, ma quello sarebbe solo l'antipattern della freccia, qualcosa che i programmatori dovrebbero evitare .
gbtimmon,

17

Come regola generale, evitare il goto è una buona idea, ma gli abusi che erano prevalenti quando Dijkstra scrisse per la prima volta "GOTO Considered Harmful" non attraversano nemmeno la mente della maggior parte delle persone come opzione in questi giorni.

Quello che delinei è una soluzione generalizzabile al problema di gestione degli errori: per me va bene fintanto che viene utilizzato con attenzione.

Il tuo esempio particolare può essere semplificato come segue (passaggio 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Continuando il processo:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

Questo è, credo, equivalente al codice originale. Questo sembra particolarmente pulito poiché il codice originale era di per sé molto pulito e ben organizzato. Spesso, i frammenti di codice non sono così ordinati (anche se accetterei l'argomento che dovrebbero esserlo); ad esempio, c'è spesso più stato da passare alle routine di inizializzazione (configurazione) di quanto mostrato, e quindi più stato da passare anche alle routine di pulizia.


23
Sì, la soluzione annidata è una delle alternative praticabili. Purtroppo diventa meno praticabile man mano che il livello di nidificazione si approfondisce.
Eli Bendersky

4
@eliben: d'accordo - ma la nidificazione più profonda potrebbe essere (probabilmente è) un'indicazione che è necessario introdurre più funzioni, o fare in modo che i passaggi di preparazione facciano di più, o altrimenti refactoring del codice. Potrei sostenere che ciascuna delle funzioni di preparazione dovrebbe eseguire la propria configurazione, chiamare la successiva nella catena ed eseguire la propria pulizia. Localizza quel lavoro: potresti persino risparmiare sulle tre funzioni di pulizia. Dipende anche, in parte, dall'utilizzo (utilizzabile) di una qualsiasi delle funzioni di configurazione o pulizia in qualsiasi altra sequenza di chiamata.
Jonathan Leffler

6
Sfortunatamente questo non funziona con i loop: se si verifica un errore all'interno di un loop, un goto è molto, molto più pulito rispetto all'alternativa di impostare e controllare flag e istruzioni "break" (che sono solo gotos abilmente mascherate).
Adam Rosenfield

1
@ Smith, più come guidare senza giubbotto antiproiettile.
strager

6
So che sto necromando qui, ma trovo questo consiglio piuttosto scadente: l' anti-pattern della freccia dovrebbe essere evitato.
King Radical

16

Sono sorpreso che nessuno abbia suggerito questa alternativa, quindi anche se la domanda è in circolazione da un po 'di tempo, la aggiungerò: un buon modo per affrontare questo problema è usare le variabili per tenere traccia dello stato corrente. Questa è una tecnica che può essere utilizzata indipendentemente dal fatto che gotovenga utilizzata per arrivare al codice di pulizia. Come ogni tecnica di codifica, ha pro e contro e non sarà adatta a ogni situazione, ma se stai scegliendo uno stile vale la pena considerare, soprattutto se vuoi evitare gotosenza finire con ifs nidificate profondamente .

L'idea di base è che, per ogni azione di pulizia che potrebbe essere necessario intraprendere, c'è una variabile dal cui valore possiamo dire se la pulizia deve essere eseguita o meno.

Mostrerò gotoprima la versione, perché è più vicina al codice nella domanda originale.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Un vantaggio di questo rispetto ad alcune delle altre tecniche è che, se l'ordine delle funzioni di inizializzazione viene modificato, la pulizia corretta continuerà ad essere eseguita - ad esempio, utilizzando il switchmetodo descritto in un'altra risposta, se l'ordine di inizializzazione cambia, allora il switchdeve essere modificato con molta attenzione per evitare di provare a ripulire qualcosa che non è stato effettivamente inizializzato in primo luogo.

Ora, alcuni potrebbero obiettare che questo metodo aggiunge molte variabili extra - e in effetti in questo caso è vero - ma in pratica spesso una variabile esistente traccia già, o può essere fatta per tracciare, lo stato richiesto. Ad esempio, se prepare_stuff()è effettivamente una chiamata a malloc(), o a open(), è possibile utilizzare la variabile che contiene il puntatore o il descrittore di file restituito, ad esempio:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

Ora, se monitoriamo ulteriormente lo stato di errore con una variabile, possiamo evitarlo del gototutto e comunque ripulirlo correttamente, senza avere un rientro che diventa sempre più profondo quanto maggiore è l'inizializzazione di cui abbiamo bisogno:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Ancora una volta, ci sono potenziali critiche su questo:

  • Non tutti quei "se" fanno male alla performance? No, perché in caso di successo, devi comunque fare tutti i controlli (altrimenti non stai controllando tutti i casi di errore); e in caso di errore la maggior parte dei compilatori ottimizzerà la sequenza dei if (oksofar)controlli falliti a un singolo salto al codice di pulizia (GCC lo fa certamente) - e in ogni caso, il caso di errore è solitamente meno critico per le prestazioni.
  • Non sta aggiungendo ancora un'altra variabile? In questo caso sì, ma spesso la return_valuevariabile può essere utilizzata per interpretare il ruolo che oksofarsta giocando qui. Se strutturi le tue funzioni per restituire errori in modo coerente, puoi persino evitare il secondo ifin ogni caso:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }

    Uno dei vantaggi di una codifica del genere è che la coerenza significa che ogni punto in cui il programmatore originale ha dimenticato di controllare il valore restituito sporge come un pollice dolente, rendendo molto più facile trovare (quella classe di) bug.

Quindi, questo è (ancora) uno stile in più che può essere utilizzato per risolvere questo problema. Usato correttamente consente un codice molto pulito e coerente - e come ogni tecnica, nelle mani sbagliate può finire per produrre codice prolisso e confuso :-)


2
Sembra che tu sia in ritardo alla festa, ma sicuramente mi piace la risposta!

Linus probabilmente rifiuterebbe il tuo codice blogs.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161: Se lo facesse, questa è una sua prerogativa - ma non sono sicuro, in base all'articolo a cui hai collegato, che sia così: nota che nello stile che sto descrivendo, (1) i condizionali non si annidano arbitrariamente profondamente, e (2) non ci sono istruzioni "if" aggiuntive rispetto a ciò di cui hai bisogno in ogni caso (supponendo che controllerai tutti i codici di ritorno).
psmears

Tanto questo invece dell'orribile goto e persino della peggiore soluzione anti-freccia!
The Paramagnetic Croissant

8

Il problema con la gotoparola chiave è per lo più frainteso. Non è palesemente malvagio. Devi solo essere consapevole dei percorsi di controllo extra che crei con ogni goto. Diventa difficile ragionare sul tuo codice e quindi sulla sua validità.

FWIW, se cerchi i tutorial developer.apple.com, adottano l'approccio goto alla gestione degli errori.

Non usiamo gotos. Viene data maggiore importanza ai valori di ritorno. La gestione delle eccezioni viene eseguita tramite setjmp/longjmp: tutto ciò che puoi.


8
Anche se ho sicuramente usato setjmp / longjmp in alcuni casi in cui è stato richiesto, lo considererei anche "peggio" di goto (che uso anche, un po 'meno riservato, quando è richiesto). Le uniche volte che uso setjmp / longjmp sono quando (1) l'obiettivo spegnerà tutto in un modo che non sarà influenzato dal suo stato attuale, o (2) l'obiettivo reinizializzerà tutto ciò che è controllato all'interno il blocco setjmp / longjmp protetto in un modo che è indipendente dal suo stato attuale.
supercat

4

Non c'è niente di moralmente sbagliato nell'affermazione goto non più di quanto ci sia qualcosa di moralmente sbagliato con i puntatori (void) *.

Dipende tutto da come usi lo strumento. Nel caso (banale) presentato, un'istruzione case può ottenere la stessa logica, anche se con un sovraccarico maggiore. La vera domanda è: "qual è il mio requisito di velocità?"

goto è semplicemente veloce, soprattutto se stai attento ad assicurarti che si compili in un breve salto. Perfetto per applicazioni in cui la velocità è un vantaggio. Per altre applicazioni, probabilmente ha senso prendere il colpo di testa con if / else + case per la manutenibilità.

Ricorda: goto non uccide le applicazioni, gli sviluppatori uccidono le applicazioni.

AGGIORNAMENTO: ecco l'esempio del caso

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 

1
Potrebbe presentare l'alternativa del "caso"? Inoltre, direi che questo è molto diverso da void *, che è richiesto per qualsiasi astrazione seria della struttura dei dati in C.Non penso che ci sia qualcuno che si opponga seriamente a void * e non troverai una singola grande base di codice senza esso.
Eli Bendersky

Re: void *, questo è esattamente il mio punto, non c'è niente di moralmente sbagliato in nessuno dei due. Esempio di interruttore / caso di seguito. int foo (int bar) {int return_value = 0; int valore_errore = 0; if (! do_something (bar)) {failure_value = 1; } altrimenti se (! init_stuff (bar)) {failure_value = 2; } else if (prepare_stuff (bar)) {{return_value = do_the_thing (bar); cleanup_3 (); } switch (valore_errore) {caso 2: cleanup_2 (); rompere ; caso 1: cleanup_1 (); rompere ; default: pausa; }}
webmarc

5
@webmarc, scusa ma è orribile! Hai appena simulato completamente goto con etichette, inventando i tuoi valori non descrittivi per le etichette e implementando goto con interruttore / caso. Failure_value = 1 è più pulito di "goto cleanup_something"?
Eli Bendersky

4
Mi sento come se mi avessi sistemato qui ... la tua domanda è per un'opinione, e cosa preferirei. Tuttavia, quando ho offerto la mia risposta, è orribile. :-( Per quanto riguarda il tuo reclamo sui nomi delle etichette, sono altrettanto descrittivi come il resto del tuo esempio: cleanup_1, foo, bar. Perché stai attaccando i nomi delle etichette quando non sono nemmeno pertinenti alla domanda?
webmarc

1
Non avevo intenzione di "creare" e provocare alcun tipo di sentimenti negativi, mi dispiace! Sembra proprio che il tuo nuovo approccio sia mirato esclusivamente alla "rimozione goto", senza aggiungere ulteriore chiarezza. È come se avessi reimplementato ciò che fa goto, usando solo più codice che è meno chiaro. Questo IMHO non è una buona cosa da fare: sbarazzarsi del goto solo per il gusto di farlo.
Eli Bendersky

2

GOTO è utile. È qualcosa che il tuo processore può fare ed è per questo che dovresti avervi accesso.

A volte vuoi aggiungere qualcosa alla tua funzione e goto singolo ti consente di farlo facilmente. Può far risparmiare tempo ..


3
Non è necessario accedere a ogni singola cosa che può fare il processore. La maggior parte delle volte goto crea più confusione rispetto all'alternativa.
David Thornley,

@DavidThornley: Sì, si fa l'accesso necessità di ogni singola cosa il processore può fare, in caso contrario, si sta sprecando il vostro processore. Goto è la migliore istruzione di programmazione.
Ron Maimon

1

In generale, considererei il fatto che un pezzo di codice potrebbe essere scritto più chiaramente usando gotocome un sintomo che il flusso del programma è probabilmente più complicato di quanto sia generalmente desiderabile. Combinare altre strutture di programma in modi strani per evitare l'uso di gototenterebbe di trattare il sintomo, piuttosto che la malattia. Il tuo esempio particolare potrebbe non essere eccessivamente difficile da implementare senzagoto :

  fare {
    .. imposta cosa1 che necessita di pulizia solo in caso di uscita anticipata
    se (errore) si rompe;
    fare
    {
      .. imposta cosa2 che dovrà essere pulito in caso di uscita anticipata
      se (errore) si rompe;
      // ***** VEDERE IL TESTO RELATIVO A QUESTA RIGA
    } while (0);
    .. cleanup thing2;
  } while (0);
  .. cleanup thing1;

ma se la pulizia doveva avvenire solo quando la funzione falliva, il gotocaso poteva essere gestito inserendo un returnappena prima della prima etichetta di destinazione. Il codice precedente richiederebbe l'aggiunta di un returnalla riga contrassegnata con *****.

Nello scenario "pulizia anche in caso normale", considererei l'uso di gotocome più chiaro dei costrutti do/ while(0), tra le altre cose perché le etichette di destinazione stesse praticamente gridano "GUARDAMI" molto più dei costrutti breake do/ while(0). Per il caso "ripulisci solo in caso di errore", l' returnistruzione finisce per trovarsi nel posto peggiore possibile dal punto di vista della leggibilità (le istruzioni di ritorno dovrebbero generalmente essere o all'inizio di una funzione, oppure a ciò che "sembra" la fine); avere returnun'etichetta appena prima di un obiettivo soddisfa quella qualifica molto più prontamente che averne una appena prima della fine di un "ciclo".

A proposito, uno scenario in cui a volte utilizzo gotoper la gestione degli errori è all'interno di switchun'istruzione, quando il codice per più casi condivide lo stesso codice di errore. Anche se il mio compilatore sarebbe spesso abbastanza intelligente da riconoscere che più casi terminano con lo stesso codice, penso che sia più chiaro dire:

 REPARSE_PACKET:
  interruttore (pacchetto [0])
  {
    caso PKT_THIS_OPERATION:
      se (condizione problematica)
        goto PACKET_ERROR;
      ... gestisci QUESTA_OPERAZIONE
      rompere;
    caso PKT_THAT_OPERATION:
      se (condizione problematica)
        goto PACKET_ERROR;
      ... gestisci QUELLA_OPERAZIONE
      rompere;
    ...
    case PKT_PROCESS_CONDITIONALLY
      if (packet_length <9)
        goto PACKET_ERROR;
      if (packet_condition che coinvolge il pacchetto [4])
      {
        packet_length - = 5;
        memmove (packet, packet + 5, packet_length);
        goto REPARSE_PACKET;
      }
      altro
      {
        pacchetto [0] = PKT_CONDITION_SKIPPED;
        packet [4] = packet_length;
        packet_length = 5;
        packet_status = READY_TO_SEND;
      }
      rompere;
    ...
    predefinito:
    {
     PACKET_ERROR:
      packet_error_count ++;
      packet_length = 4;
      pacchetto [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      rompere;
    }
  }   

Sebbene si possano sostituire le gotoistruzioni con {handle_error(); break;}, e sebbene si possa usare un ciclo do/ while(0)insieme a continueper elaborare il pacchetto di esecuzione condizionale racchiuso, non credo sia più chiaro dell'uso di un file goto. Inoltre, mentre potrebbe essere possibile copiare il codice da PACKET_ERRORqualsiasi luogo in cui goto PACKET_ERRORviene utilizzato, e mentre un compilatore potrebbe scrivere il codice duplicato una volta e sostituire la maggior parte delle occorrenze con un salto a quella copia condivisa, l'uso di gotorende più facile notare i luoghi che imposta il pacchetto in modo leggermente diverso (ad esempio se l'istruzione "esegui in modo condizionale" decide di non eseguire).


1

Personalmente sono un seguace del "The Power of Ten - 10 regole per la scrittura di codice critico per la sicurezza" .

Includerò un piccolo frammento di quel testo che illustra ciò che credo sia una buona idea di goto.


Regola: limita tutto il codice a costrutti di flusso di controllo molto semplici: non utilizzare istruzioni goto, costrutti setjmp o longjmp e ricorsione diretta o indiretta.

Motivazione: un flusso di controllo più semplice si traduce in capacità di verifica più forti e spesso si traduce in una migliore chiarezza del codice. L'abolizione della ricorsione è forse la più grande sorpresa qui. Senza ricorsione, tuttavia, abbiamo la garanzia di avere un grafo di chiamata di funzione aciclico, che può essere sfruttato dagli analizzatori di codice, e può aiutare direttamente a dimostrare che tutte le esecuzioni che dovrebbero essere limitate sono di fatto limitate. (Si noti che questa regola non richiede che tutte le funzioni abbiano un unico punto di ritorno, sebbene questo spesso semplifichi anche il flusso di controllo. Ci sono abbastanza casi, tuttavia, in cui un ritorno di errore anticipato è la soluzione più semplice.)


Bandire l'uso di goto sembra male ma:

Se le regole all'inizio sembrano draconiane, tieni presente che hanno lo scopo di rendere possibile il controllo del codice dove letteralmente la tua vita può dipendere dalla sua correttezza: codice che viene utilizzato per controllare l'aereo su cui voli, la centrale nucleare a pochi chilometri da dove vivi, o dalla navicella che trasporta gli astronauti in orbita. Le regole si comportano come le cinture di sicurezza della tua auto: all'inizio forse sono un po 'scomode, ma dopo poco il loro utilizzo diventa una seconda natura e non usarle diventa inimmaginabile.


22
Il problema con questo è che il modo usuale di bandire completamente il gotoè usare un insieme di booleani "intelligenti" in if o loop profondamente annidati. Questo non aiuta davvero. Forse i tuoi strumenti funzioneranno meglio, ma non lo farai e tu sei più importante.
Donal Fellows

1

Sono d'accordo che la pulizia goto nell'ordine inverso dato nella domanda è il modo più pulito di pulire le cose nella maggior parte delle funzioni. Ma volevo anche sottolineare che a volte vuoi che la tua funzione venga pulita comunque. In questi casi utilizzo la seguente variante if if (0) {label:} idioma per andare al punto giusto del processo di pulizia:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

0

Mi sembra che cleanup_3dovrebbe fare la sua pulizia, quindi chiamare cleanup_2. Allo stesso modo, cleanup_2dovrebbe fare la pulizia, quindi chiamare cleanup_1. Sembra che ogni volta che lo fai cleanup_[n], ciò cleanup_[n-1]è richiesto, quindi dovrebbe essere responsabilità del metodo (in modo che, ad esempio, cleanup_3non possa mai essere chiamato senza chiamare cleanup_2e possibilmente causare una perdita.)

Dato questo approccio, invece di gotos, dovresti semplicemente chiamare la routine di pulizia, quindi tornare.

L' gotoapproccio non è sbagliato o cattivo , però, vale solo la pena notare che non è necessariamente l'approccio "più pulito" (IMHO).

Se stai cercando le prestazioni ottimali, suppongo che la gotosoluzione sia la migliore. Mi aspetto che sia rilevante, tuttavia, in alcune applicazioni critiche per le prestazioni (ad esempio, driver di dispositivo, dispositivi incorporati, ecc.). Altrimenti, è una microottimizzazione che ha una priorità inferiore rispetto alla chiarezza del codice.


4
Ciò non taglierà: le pulizie sono specifiche per le risorse, che sono assegnate in questo ordine solo in questa routine. In altri posti, non sono correlati, quindi chiamare l'uno dall'altro non ha senso.
Eli Bendersky

0

Penso che la domanda qui sia fallace rispetto al codice dato.

Tener conto di:

  1. do_something (), init_stuff () e prepare_stuff () sembrano sapere se hanno fallito, dal momento che in questo caso restituiscono false o nul.
  2. La responsabilità per l'impostazione dello stato sembra essere la responsabilità di quelle funzioni, poiché non esiste uno stato impostato direttamente in foo ().

Pertanto: do_something (), init_stuff () e prepare_stuff () dovrebbero eseguire la propria pulizia . Avere una funzione cleanup_1 () separata che ripulisce dopo do_something () infrange la filosofia dell'incapsulamento. È un cattivo design.

Se hanno fatto la loro pulizia, allora foo () diventa abbastanza semplice.

D'altro canto. Se foo () ha effettivamente creato il proprio stato che doveva essere abbattuto, goto sarebbe appropriato.


0

Ecco cosa ho preferito:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

0

Vecchia discussione, tuttavia ... che ne dici di usare "freccia anti pattern" e incapsulare in seguito ogni livello annidato in una funzione inline statica? Il codice sembra pulito, è ottimale (quando le ottimizzazioni sono abilitate) e non viene utilizzato goto. In breve, dividi e conquista. Di seguito un esempio:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

In termini di spazio, stiamo creando tre volte la variabile nello stack, il che non va bene, ma questo scompare quando si compila con -O2 rimuovendo la variabile dallo stack e utilizzando un registro in questo semplice esempio. Quello che ho ottenuto dal blocco sopra è gcc -S -O2 test.cstato il seguente:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-1

Preferisco usare la tecnica descritta nel seguente esempio ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

fonte: http://blog.staila.com/?p=114


2
il codice flaggy e l'anti-pattern freccia (entrambi mostrati nel tuo esempio) sono entrambe cose che complicano inutilmente il codice. Non c'è alcuna giustificazione al di fuori di "goto is evil" per usarli.
KingRadical

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.