È un caso d'uso decente per andare in C?


59

Esito davvero a chiederlo, perché non voglio "sollecitare dibattiti, argomentazioni, sondaggi o discussioni estese", ma sono nuovo di C e voglio approfondire gli schemi comuni utilizzati nella lingua.

Di recente ho sentito un po 'di disgusto per il gotocomando, ma recentemente ho anche trovato un caso d'uso decente per questo.

Codice come questo:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

Se la parte di pulizia è molto simile, potrebbe essere scritta un po 'più carina (la mia opinione?) In questo modo:

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

È un caso d'uso comune o accettabile di gotoin C? C'è un modo diverso / migliore per farlo?



2
Più che rivestimento di zucchero, il compilatore garantisce la pulizia e il controllo degli errori, mentre in C è necessario farlo manualmente, violando DRY e garantendo che in qualche punto eri troppo pigro per controllare il codice di errore o andare sull'etichetta sbagliata o qualcosa del genere.
DeadMG

3
@Izkata: No, il punto è avere funzioni (come i distruttori) che vengono chiamate implicitamente dal compilatore quando viene generata un'eccezione.
DeadMG

6
@DeadMG: l'uso di un'altra lingua non è sempre un'opzione.
Benjamin Kloster,

13
Penso che con un'adeguata denominazione dell'etichetta, l'esempio può sembrare abbastanza decente:goto hell;
moscerino

Risposte:


48

L' gotoistruzione (e le etichette corrispondenti) sono una primitiva di controllo del flusso (insieme all'esecuzione condizionale di un'istruzione). Con ciò, intendo dire che sono lì per permetterti di costruire reti di controllo del flusso del programma. Puoi pensare a loro come a modellare le frecce tra i nodi di un diagramma di flusso.

Alcuni di questi possono essere ottimizzati immediatamente, dove esiste un flusso lineare diretto (basta usare una sequenza di istruzioni di base). Altri schemi sono meglio sostituiti con costrutti di programmazione strutturati dove sono disponibili; se sembra un whileloop, usa un whileloop , ok? Gli schemi di programmazione strutturati sono sicuramente almeno potenzialmente più chiari di intenti di un pasticcio di gotodichiarazioni.

Eppure C non include tutti i possibili costrutti di programmazione strutturata. (Non è chiaro per me che tutti quelli rilevanti sono stati ancora scoperti; il tasso di scoperta è lento ora, ma esiterei a saltare dicendo che sono stati trovati tutti.) Di quelli che conosciamo, C sicuramente manca il try/ catch/ finallystruttura (e anche eccezioni). Manca anche di multi-livello break-from-loop. Questi sono i tipi di cose che gotopossono essere utilizzati per implementare. È possibile utilizzare anche altri schemi per fare questo - sappiamo che C ha un set sufficiente di non-gotoprimitive - ma spesso implicano la creazione di variabili flag e condizioni di loop o di guardia molto più complesse; aumentare l'entanglement dell'analisi di controllo con l'analisi dei dati rende il programma più difficile da comprendere nel complesso. Inoltre, rende più difficile l'ottimizzazione del compilatore e l'esecuzione rapida della CPU (la maggior parte dei costrutti di controllo del flusso - e sicuramente goto - sono molto economici).

Pertanto, anche se non dovresti usarlo a gotomeno che non sia necessario, dovresti essere consapevole che esiste e che potrebbe essere necessario, e se ne hai bisogno, non dovresti sentirti troppo male. Un esempio di un caso in cui è necessario è la deallocazione delle risorse quando una funzione chiamata restituisce una condizione di errore. (Cioè, try/ finally.) È possibile scrivere che senza, gotoma farlo, può avere dei lati negativi, come i problemi di mantenerlo. Un esempio del caso:

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

Il codice potrebbe essere ancora più breve, ma è sufficiente per dimostrare il punto.


4
+1: In C goto tecnicamente non è mai "necessario" - c'è sempre un modo per farlo, diventa disordinato ..... Per un solido set di linee guida per l'uso di goto guarda MISRA C.
mattnz

1
Preferiresti try/catch/finallya gotodispetto della forma simile, ma più pervasiva (in quanto può diffondersi attraverso più funzioni / moduli) di codice spaghetti che è possibile utilizzare try/catch/finally?
autistico

65

Sì.

È usato, ad esempio, nel kernel Linux. Ecco un'e-mail dalla fine di un thread di quasi un decennio fa , che mette in grassetto il mio:

Da: Robert Love
Oggetto: Ri: qualche possibilità di 2.6.0-test *?
Data: 12 gen 2003 17:58:06 -0500

In domenica, 2003-01-12 alle 17:22, Rob Wilkens ha scritto:

Dico "per favore non usare goto" e invece ho una funzione "cleanup_lock" e la aggiungo prima di tutte le dichiarazioni di ritorno. Non dovrebbe essere un onere. Sì, sta chiedendo allo sviluppatore di lavorare un po 'di più, ma il risultato finale è un codice migliore.

No, è disgustoso e gonfia il kernel . Include un mucchio di spazzatura per N percorsi di errore, invece di avere il codice di uscita una volta alla fine. L'impronta della cache è la chiave e l'hai appena uccisa.

Né è più facile da leggere.

Come ultimo argomento, non ci consente di fare in modo pulito il solito vento in pila e di rilassarci , ad es

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

Adesso basta.

Robert Love

Detto questo, richiede molta disciplina per impedirti di creare il codice spaghetti una volta che ti sei abituato a usare goto, quindi a meno che tu non stia scrivendo qualcosa che richiede velocità e un ingombro di memoria ridotto (come un kernel o un sistema incorporato) dovresti davvero pensaci prima di scrivere il primo goto.


21
Si noti che un kernel è diverso da un programma non kernel per quanto riguarda la priorità sulla velocità grezza rispetto alla leggibilità. In altre parole, hanno già profilato e scoperto che hanno bisogno di ottimizzare il loro codice per la velocità con goto.

11
Utilizzo dello stack un-wind per gestire la pulizia in caso di errore senza effettivamente spingere sullo stack! Questo è un fantastico uso di goto.
mike30,

1
@ user1249, Rubbish, non puoi profilare tutte le app {pass, esistenti, future} che usano un pezzo di {libreria, kernel} codice. Devi semplicemente essere veloce.
Pacerier,

1
Non correlato: sono stupito di come le persone possano usare le mailing list per fare qualsiasi cosa, figuriamoci progetti così grandi. È così ... primitivo. Come fanno le persone a entrare nella caserma dei messaggi ?!
Alexander

2
Non sono così preoccupato per la moderazione. Se qualcuno è abbastanza morbido da essere respinto da uno stronzo su Internet, il tuo progetto probabilmente è meglio senza di loro. Sono più preoccupato per l'impraticabilità di stare al passo con la raffica di messaggi in arrivo, e di come potresti avere una discussione a ritroso naturale con così pochi strumenti per tenere traccia delle citazioni, per esempio.
Alexander,

14

A mio avviso, il codice che hai pubblicato è un esempio di un uso valido di goto, perché salti solo verso il basso e lo usi solo come un gestore di eccezioni primitivo.

Tuttavia , a causa del vecchio dibattito goto, i programmatori hanno evitato gotoper circa 40 anni e quindi non sono abituati a leggere il codice con goto. Questo è un motivo valido per evitare goto: semplicemente non è lo standard.

Avrei riscritto il codice come qualcosa di più facilmente letto dai programmatori C:

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

Vantaggi di questo design:

  • La funzione che svolge il lavoro effettivo non deve occuparsi di attività irrilevanti per il suo algoritmo, come l'allocazione dei dati.
  • Il codice apparirà meno estraneo ai programmatori C, poiché hanno paura di andare a goto ed etichette.
  • È possibile centralizzare la gestione degli errori e la deallocazione nello stesso punto, al di fuori della funzione che esegue l'algoritmo. Non ha senso che una funzione gestisca i propri risultati.


9

In Java lo faresti così:

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

Lo uso molto. Per quanto non mi piaccia goto, nella maggior parte degli altri linguaggi in stile C uso il tuo codice; non c'è altro buon modo per farlo. (Saltare da loop nidificati è un caso simile; in Java utilizzo un etichettato breake ovunque uso un goto.)


3
Aw che è una struttura di controllo ordinata.
Bryan Boettcher,

4
Questo è davvero interessante Normalmente penserei di usare la struttura try / catch / finally per questo in Java (lanciare eccezioni invece di rompere).
Robz,

5
È davvero illeggibile (almeno per me). Se presenti, le eccezioni sono molto migliori.
m3th0dman,

1
@ m3th0dman Sono d'accordo con te in questo esempio particolare (gestione degli errori). Ma ci sono altri casi (non eccezionali) in cui questo linguaggio potrebbe tornare utile.
Konrad Rudolph,

1
Le eccezioni sono costose, devono generare un errore, stacktrace e molta più spazzatura. Questa rottura dell'etichetta dà una chiara uscita da un ciclo di controllo. a meno che non ci si interessi della memoria e della velocità, quindi per quanto mi riguarda usare un'eccezione.
Tschallacka,

8

Penso che sia un caso d'uso decente, ma nel caso in cui "errore" non sia altro che un valore booleano, esiste un modo diverso di ottenere ciò che si desidera:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

Questo si avvale della valutazione del corto circuito degli operatori booleani. Se questo "migliore", dipende dai tuoi gusti personali e da come sei abituato a quel linguaggio.


1
Il problema è che erroril valore potrebbe diventare insignificante con tutti gli OR.
James,

@James: modificata la mia risposta a causa del tuo commento
Doc Brown

1
Questo non è sufficiente. Se si è verificato un errore durante la prima funzione, non voglio eseguire la seconda o la terza funzione.
Robz,

2
Se con la breve mano di valutazione si intende corto circuito di valutazione, questo non è esattamente ciò che è fatto qui a causa dell'uso di OR bit a bit invece di logica OR.
Chris dice di reintegrare Monica il

1
@ChristianRau: grazie, ho modificato la mia risposta di conseguenza
Doc Brown,

6

La guida di stile linux fornisce ragioni specifiche per usare quelle gotoche sono in linea con il tuo esempio:

https://www.kernel.org/doc/Documentation/process/coding-style.rst

La logica per l'utilizzo di gotos è:

  • le dichiarazioni incondizionate sono più facili da capire e da seguire
  • la nidificazione è ridotta
  • si evitano errori non aggiornando i singoli punti di uscita quando si apportano modifiche
  • salva il lavoro del compilatore per ottimizzare il codice ridondante;)

Disclaimer Non dovrei condividere il mio lavoro. Gli esempi qui sono un po 'inventati, quindi sopportatemi, per favore, abbiate pazienza.

Questo è buono per la gestione della memoria. Di recente ho lavorato su un codice che aveva allocato dinamicamente la memoria (ad esempio un char *restituito da una funzione). Una funzione che osserva un percorso e accerta se il percorso è valido analizzando i token del percorso:

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

Ora per me, il seguente codice è molto più bello e più facile da mantenere se è necessario aggiungere un varNplus1:

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

Ora il codice aveva ogni sorta di altri problemi con esso, vale a dire che N era da qualche parte sopra 10, e la funzione era di oltre 450 linee, con 10 livelli di annidamento in alcuni punti.

Ma ho offerto al mio supervisore di riformattarlo, cosa che ho fatto e ora è un mucchio di funzioni che sono tutte brevi e tutte hanno lo stile linux

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

Se consideriamo l'equivalente senza gotos:

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

Per me, nel primo caso, è ovvio che se la prima funzione ritorna NULL, siamo fuori di qui e stiamo tornando 0. Nel secondo caso, devo scorrere verso il basso per vedere che if contiene l'intera funzione. Concesso il primo mi indica questo stilisticamente (il nome " out") e il secondo lo fa sintatticamente. Il primo è ancora più ovvio.

Inoltre, preferisco di gran lunga avere delle free()dichiarazioni alla fine di una funzione. Questo in parte perché, nella mia esperienza, le free()dichiarazioni nel mezzo di funzioni hanno un cattivo odore e mi indicano che dovrei creare una subroutine. In questo caso, ho creato la var1mia funzione e non potevo free()farlo in una subroutine, ma è per questo che lo goto out_freestile, goto out è così pratico.

Penso che i programmatori debbano essere educati credendo che gotosiano cattivi. Quindi, quando sono abbastanza maturi, dovrebbero sfogliare il codice sorgente di Linux e leggere la guida di stile di Linux.

Dovrei aggiungere che uso questo stile in modo molto coerente, ogni funzione ha un int retval, out_freeun'etichetta e un'etichetta out. A causa della coerenza stilistica, la leggibilità è migliorata.

Bonus: si interrompe e continua

Supponi di avere un ciclo while

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

Ci sono altre cose che non vanno in questo codice, ma una cosa è l'istruzione continue. Vorrei riscrivere il tutto, ma mi è stato assegnato il compito di modificarlo in piccolo. Mi ci sarebbero voluti giorni per riformattarlo in un modo che mi soddisfacesse, ma il cambiamento effettivo è stato di circa mezza giornata di lavoro. Il problema è che anche se " continue" dobbiamo ancora liberarci var1e var2. Ho dovuto aggiungere un var3, e mi ha fatto venire voglia di rispecchiare le dichiarazioni free ().

All'epoca ero un tirocinante relativamente nuovo, ma da un po 'di tempo guardavo il codice sorgente di Linux per divertimento, quindi ho chiesto al mio supervisore se potevo usare un'istruzione goto. Ha detto di sì, e ho fatto questo:

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

Penso che i continui siano nella migliore delle ipotesi ma per me sono come un goto con un'etichetta invisibile. Lo stesso vale per le pause. Preferirei continuare o interrompere, a meno che, come nel caso qui, non ti costringa a rispecchiare le modifiche in più punti.

E dovrei anche aggiungere che questo uso goto next;e l' next:etichetta non sono soddisfacenti per me. Sono semplicemente meglio che rispecchiare free()le count++affermazioni e quelle .

gotoHanno quasi sempre torto, ma bisogna sapere quando sono buoni da usare.

Una cosa che non ho discusso è la gestione degli errori che è stata coperta da altre risposte.

Prestazione

Si può guardare all'implementazione di strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

Per favore, correggimi se sbaglio, ma credo che l' cont:etichetta e la goto cont;dichiarazione siano lì per le prestazioni (sicuramente non rendono il codice più leggibile). Potrebbero essere sostituiti con codice leggibile facendo

while( isDelim(*s++,delim));

per saltare i delimitatori. Ma per essere il più veloci possibile ed evitare chiamate di funzione non necessarie, lo fanno in questo modo.

Ho letto l'articolo di Dijkstra e lo trovo abbastanza esoterico.

google "dichiarazione di dijkstra goto considerata dannosa" perché non ho abbastanza reputazione per pubblicare più di 2 link.

L'ho visto citato come un motivo per non usare goto e leggerlo non ha cambiato nulla per quanto riguarda i miei usi di goto.

Addendum :

Ho escogitato una regola ordinata mentre penso a tutto ciò su continui e interruzioni.

  • Se in un ciclo while, hai un continue, allora il corpo del ciclo while dovrebbe essere una funzione e il continue dovrebbe essere un'istruzione return.
  • Se in un ciclo while, hai un'istruzione break, allora il ciclo while stesso dovrebbe essere una funzione e l'interruzione dovrebbe diventare un'istruzione return.
  • Se hai entrambi, qualcosa potrebbe essere sbagliato.

Non è sempre possibile a causa di problemi di ambito, ma ho scoperto che farlo rende molto più facile ragionare sul mio codice. Avevo notato che ogni volta che un ciclo di tempo aveva una pausa o una continuazione mi dava una brutta sensazione.


2
+1 ma posso non essere d'accordo su un punto? "Penso che i programmatori debbano essere cresciuti credendo che i goto siano cattivi". Forse è così, ma ho imparato per la prima volta a programmare in BASIC, con numeri di riga e GOTO, senza un editor di testo, nel 1975. Ho incontrato la programmazione strutturata dieci anni dopo, dopo di che mi ci è voluto un mese per smettere di usare GOTO da solo, senza qualsiasi pressione per fermare. Oggi uso GOTO alcune volte all'anno per vari motivi, ma non mi viene in mente molto. Non essere stato educato a credere che GOTO sia malvagio non mi ha fatto alcun danno che io sappia, e potrebbe anche aver fatto del bene. Sono solo io.
thb

1
Penso che tu abbia ragione su questo. Mi è venuta l'idea che i GOTO non dovevano essere utilizzati e, per puro caso, stavo navigando sul codice sorgente di Linux in un momento in cui stavo lavorando su un codice che aveva queste funzioni con più punti di uscita con memoria da liberare. Altrimenti, non avrei mai saputo di queste tecniche.
Philippe Carphin,

1
@thb Inoltre, storia divertente, ho chiesto al mio supervisore in quel momento, come stagista, il permesso di usare GOTO e mi sono assicurato di spiegargli che li avrei usati in un modo specifico come il modo in cui è usato nel Il kernel di Linux e ha detto "Ok, ha senso, e inoltre, non sapevo che potessi usare GOTO in C".
Philippe Carphin,

1
@thb Non so se è bello andare in loop (invece di romperli) come questo ? Bene, è un cattivo esempio, ma trovo che il quicksort con dichiarazioni goto (esempio 7a) sulla Programmazione strutturata di Knuth con Dichiarazioni non sia molto comprensibile.
Yai0Phah,

@ Yai0Phah Spiegherò il mio punto, ma la mia spiegazione non diminuisce il tuo bell'esempio 7a! Approvo l'esempio. Tuttavia, i padroni di casa prepotenti amano insegnare alla gente su Goto. È difficile trovare un uso pratico di goto dal 1985 che causi problemi significativi, mentre si possono trovare goto innocui che facilitano il lavoro del programmatore. Andiamo così di rado nella programmazione moderna, comunque, che, quando si presenta, il mio consiglio è, se vuoi usarlo, allora probabilmente dovresti semplicemente usarlo. Va bene. Il problema principale con goto è che alcuni credono che deprecare Goto li faccia sembrare intelligenti.
THB

5

Personalmente lo farei refactoring più come questo:

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

Ciò sarebbe più motivato evitando l'annidamento profondo piuttosto che evitare il goto (IMO è un problema peggiore con il primo esempio di codice) e ovviamente dipenderà dal fatto che CleanupAfterError sia possibile al di fuori dell'ambito (in questo caso "params" potrebbe essere una struttura contenente una memoria allocata che è necessario liberare, un FILE * che è necessario chiudere o altro).

Un grande vantaggio che vedo con questo approccio è che è sia più facile che più pulito inserire un ipotetico passo aggiuntivo futuro tra, diciamo, FTCF2 e FTCF3 (o rimuovere un passaggio attuale esistente), quindi si presta meglio alla manutenibilità (e alla persona che eredita il mio codice non volendo linciarmi!) - vai a parte, la versione nidificata manca.


1
Non ho dichiarato questo nella mia domanda, ma è possibile che gli FTCF NON abbiano gli stessi parametri, rendendo questo schema un po 'più complicato. Grazie comunque.
Robz,

3

Dai un'occhiata alle linee guida di codifica C MISRA (Motor Industry Software Reliability Association) che consentono di passare a criteri rigorosi (che il tuo esempio soddisfa)

Dove lavoro lo stesso codice verrebbe scritto - non c'è bisogno di andare - Evitare inutili dibattiti religiosi su di loro è un grande vantaggio in qualsiasi software house.

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

o per "goto in drag" - qualcosa di ancora più complicato di goto, ma aggira il "No goto Ever !!!" campo) "Sicuramente deve essere OK, non usa Goto" ....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

Se le funzioni hanno lo stesso tipo di parametro, inseriscile in una tabella e usa un ciclo:


2
Le attuali linee guida MISRA-C: 2004 non consentono di andare in alcun modo (vedere la regola 14.4). Nota che il comitato MISRA è sempre stato confuso al riguardo, non sanno su quale piede stare. In primo luogo, hanno vietato incondizionatamente l'uso di goto, continua ecc. Ma nella bozza per l'imminente MISRA 2011 vogliono permetterli di nuovo. Come sidenote, si noti che MISRA vieta l'assegnazione all'interno di if-statement, per ottime ragioni poiché è molto più pericolosa di qualsiasi uso di goto.

1
Da un punto di vista analitico, aggiungere un flag a un programma equivale a duplicare tutto il codice di codice in cui il flag è nell'ambito, avere ogni if(flag)copia in uno prende il ramo "if" e avere ogni istruzione corrispondente nell'altra copia prende il " altro". Le azioni che impostano e cancellano la bandiera sono in realtà "goto" che saltano tra quelle due versioni del codice. Ci sono momenti in cui l'uso delle bandiere è più pulito di qualsiasi alternativa, ma l'aggiunta di una bandiera per salvare un gotobersaglio non è un buon compromesso.
supercat

1

Uso anche gotose la do/while/continue/breakpirateria informatica alternativa sarebbe meno leggibile.

gotos hanno il vantaggio che i loro bersagli hanno un nome e leggono goto something;. Questo potrebbe essere più leggibile di breako continuese non stai effettivamente fermando o continuando qualcosa.


4
Ovunque all'interno di un do ... while(0)o un altro costrutto che non è un loop reale ma un tentativo sfrenato di impedire l'uso di a goto.
aib

1
Ah, grazie, non conoscevo questo particolare marchio di "Perché diavolo qualcuno dovrebbe farlo ?!" costruisce ancora.
Benjamin Kloster,

2
Di solito, l'hacker do / while / continue / break diventa illeggibile solo quando il modulo che lo contiene è troppo lungo dapprima.
John R. Strohm,

2
Non riesco a trovare nulla in questo come giustificazione per usare goto. Interrompere e continuare hanno una conseguenza ovvia. goto ... dove? Dov'è l'etichetta? Break and goto ti dice esattamente dove si trova il passaggio successivo e nelle vicinanze.
Rig

1
L'etichetta dovrebbe ovviamente essere visibile all'interno del loop. Sono d'accordo con la parte lungimirante del commento di @John R. Strohm. E il tuo punto, tradotto in hacking a ciclo continuo, diventa "Rompere da cosa? Questo non è un ciclo!". In ogni caso, questo sta diventando ciò che l'OP temeva potesse, quindi sto abbandonando la discussione.
agosto

-1
for (int y=0; y<height; ++y) {
    for (int x=0; x<width; ++x) {
        if (find(x, y)) goto found;
    }
}
found:

Se c'è solo un loop, breakfunziona esattamente come goto, sebbene non abbia alcuno stigma.
9000

6
-1: In primo luogo, xey sono FUORI CAMPO di ricerca :, quindi questo non ti aiuta. In secondo luogo, con il codice scritto, il fatto che tu sia arrivato ha trovato: non significa che hai trovato quello che stavi cercando.
John R. Strohm,

È perché questo è l'esempio più piccolo che mi viene in mente per il caso di uscire da un numero multiplo di loop. Non esitate a modificarlo per un'etichetta migliore o un controllo fatto.
agosto

1
Ma tieni anche presente che le funzioni C non sono necessariamente prive di effetti collaterali.
agosto

1
@ JohnR.Strohm Questo non ha molto senso ... L'etichetta 'found' viene utilizzata per interrompere il ciclo, non per controllare le variabili. Se volessi controllare le variabili potrei fare qualcosa del genere: for (int y = 0; y <height; ++ y) {for (int x = 0; x <width; ++ x) {if (find ( x, y)) {doSomeThingWith (x, y); trovato; }}} trovato:
YoYoYonnY il

-1

Ci saranno sempre campi che dicono che un modo è accettabile e un altro no. Le aziende per cui ho lavorato hanno aggrottato le sopracciglia o fortemente sconsigliato l'uso. Personalmente, non riesco a pensare a quando ne ho usato uno, ma ciò non significa che siano cattivi , solo un altro modo di fare le cose.

In C, di solito eseguo le seguenti operazioni:

  • Test per condizioni che potrebbero impedire l'elaborazione (input errati, ecc.) E "ritorno"
  • Esegui tutti i passaggi che richiedono l'allocazione delle risorse (ad es. Malloc)
  • Eseguire l'elaborazione, in cui più passaggi verificano il successo
  • Rilasciare tutte le risorse, se allocate correttamente
  • Restituisce tutti i risultati

Per l'elaborazione, usando il tuo esempio goto, farei questo:

errore = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }

Non esiste nidificazione e all'interno delle clausole if, è possibile eseguire qualsiasi segnalazione di errore, se il passaggio ha generato un errore. Quindi, non deve essere "peggio" di un metodo che usa goto.

Devo ancora imbattermi in un caso in cui qualcuno ha goto che non può essere fatto con un altro metodo ed è altrettanto leggibile / comprensibile e questa è la chiave, IMHO.

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.