Domanda

Ne ho da tempo l'impressione goto non dovrebbe mai essere usato, se possibile.L'altro giorno, mentre esaminavo libavcodec (che è scritto in C), ne ho notato molteplici usi.È mai vantaggioso da usare goto in un linguaggio che supporta loop e funzioni?Se sì, perché?

È stato utile?

Soluzione

Ci sono alcuni motivi per usare l'istruzione "goto" di cui sono a conoscenza (alcuni ne hanno già parlato):

Uscita pulita da una funzione

Spesso in una funzione è possibile allocare risorse e dover uscire in più punti.I programmatori possono semplificare il proprio codice inserendo il codice di pulizia delle risorse alla fine della funzione e tutti i "punti di uscita" della funzione andrebbero all'etichetta di pulizia.In questo modo non è necessario scrivere codice di pulizia in ogni "punto di uscita" della funzione.

Uscita dai cicli nidificati

Se ti trovi in ​​un ciclo annidato e devi uscirne Tutto loop, un goto può renderlo molto più pulito e semplice rispetto alle istruzioni break e ai controlli if.

Miglioramenti delle prestazioni di basso livello

Questo è valido solo nel codice perf-critical, ma le istruzioni goto vengono eseguite molto rapidamente e possono darti una spinta quando ti muovi attraverso una funzione.Questa è tuttavia un'arma a doppio taglio, poiché un compilatore in genere non è in grado di ottimizzare il codice che contiene goto.

Si noti che in tutti questi esempi i goto sono limitati all'ambito di una singola funzione.

Altri suggerimenti

Tutti quelli che sono anti-goto cita, direttamente o indirettamente, quello di Edsger Dijkstra GoTo considerato dannoso articolo per corroborare la propria posizione.Peccato che l'articolo di Dijkstra lo abbia virtualmente Niente a che fare con il modo goto Al giorno d'oggi vengono utilizzate dichiarazioni e quindi ciò che dice l'articolo ha poca o nessuna applicabilità alla scena della programmazione moderna.IL goto-less meme rasenta ora una religione, fino alle sue scritture dettate dall'alto, ai suoi sommi sacerdoti e all'evitamento (o peggio) dei presunti eretici.

Mettiamo l'articolo di Dijkstra nel contesto per fare un po' di luce sull'argomento.

Quando Dijkstra scrisse il suo articolo, i linguaggi popolari dell'epoca erano linguaggi procedurali non strutturati come BASIC, FORTRAN (i dialetti precedenti) e vari linguaggi assembly.Era abbastanza comune che le persone che usavano le lingue di livello superiore saltassero in tutta la loro base di codice in fili di esecuzione contorti e contorti che hanno dato origine al termine "spaghetti code".Puoi vederlo saltando su il classico gioco Trek scritto da Mike Mayfield e cercando di capire come funzionano le cose.Prenditi qualche minuto per esaminarlo.

QUESTO è "l'uso sfrenato dell'istruzione go to" contro cui Dijkstra si scagliava nel suo articolo nel 1968. QUESTO è l'ambiente in cui viveva che lo portò a scrivere quel saggio.La capacità di saltare ovunque nel codice in qualsiasi punto tu volessi era ciò che criticava e chiedeva di essere fermato.Confrontandolo con i poteri anemici di goto in C o in altri linguaggi più moderni è semplicemente ridicolo.

Posso già sentire i canti elevati dei cultisti mentre affrontano l'eretico."Ma", canteranno, "puoi rendere il codice molto difficile da leggere goto in Do." Ah sì?È possibile rendere il codice molto difficile da leggere senza goto anche.Come questo:

#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
            _-_-_-_
       _-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_
    _-_-_-_-_-_-_-_-_-_-_-_
        _-_-_-_-_-_-_-_
            _-_-_-_
}

Non un goto in vista, quindi deve essere facile da leggere, giusto?O che ne pensi di questo:

a[900];     b;c;d=1     ;e=1;f;     g;h;O;      main(k,
l)char*     *l;{g=      atoi(*      ++l);       for(k=
0;k*k<      g;b=k       ++>>1)      ;for(h=     0;h*h<=
g;++h);     --h;c=(     (h+=g>h     *(h+1))     -1)>>1;
while(d     <=g){       ++O;for     (f=0;f<     O&&d<=g
;++f)a[     b<<5|c]     =d++,b+=    e;for(      f=0;f<O
&&d<=g;     ++f)a[b     <<5|c]=     d++,c+=     e;e= -e
;}for(c     =0;c<h;     ++c){       for(b=0     ;b<k;++
b){if(b     <k/2)a[     b<<5|c]     ^=a[(k      -(b+1))
<<5|c]^=    a[b<<5      |c]^=a[     (k-(b+1     ))<<5|c]
;printf(    a[b<<5|c    ]?"%-4d"    :"    "     ,a[b<<5
|c]);}      putchar(    '\n');}}    /*Mike      Laman*/

NO goto neanche lì.Deve quindi essere leggibile.

Qual è il punto con questi esempi?Non sono le caratteristiche del linguaggio a rendere il codice illeggibile e non gestibile.Non è la sintassi a farlo.Sono i cattivi programmatori a causare questo.E i cattivi programmatori, come puoi vedere nell'articolo sopra, possono creare Qualunque caratteristica del linguaggio illeggibile e inutilizzabile.Come il for gira lassù.(Puoi vederli, vero?)

Ora, per essere onesti, è più facile abusare di alcuni costrutti linguistici rispetto ad altri.Se sei un programmatore C, tuttavia, osserverei molto più da vicino circa il 50% degli usi di #define molto prima di intraprendere una crociata contro goto!

Quindi, per coloro che si sono presi la briga di leggere fin qui, ci sono diversi punti chiave da notare.

  1. L'articolo di Dijkstra su goto istruzioni sono state scritte per un ambiente di programmazione in cui goto era un quantitàpiù potenzialmente dannoso di quanto non lo sia nella maggior parte dei linguaggi moderni che non sono un assemblatore.
  2. Buttare via automaticamente tutti gli usi di goto per questo è razionale quanto dire "Ci ho provato per divertirmi una volta ma non mi è piaciuto, quindi ora sono contrario".
  3. Esistono usi legittimi del moderno (anemico) goto istruzioni nel codice che non possono essere adeguatamente sostituito da altri costrutti.
  4. Esistono, ovviamente, usi illegittimi delle stesse affermazioni.
  5. Esistono anche usi illegittimi delle moderne dichiarazioni di controllo come "godo" abominio dove è sempre falso do il ciclo è interrotto dall'utilizzo break al posto di a goto.Questi sono spesso peggiori di un uso giudizioso goto.

Obbedire ciecamente alle migliori pratiche non è una buona pratica.L'idea di evitare goto istruzioni come forma primaria di controllo del flusso è evitare di produrre spaghetti code illeggibili.Se usati con parsimonia nei posti giusti, a volte possono essere il modo più semplice e chiaro per esprimere un'idea.Walter Bright, il creatore del compilatore Zortech C++ e del linguaggio di programmazione D, li usa frequentemente, ma con giudizio.Anche con il goto dichiarazioni, il suo codice è ancora perfettamente leggibile.

Linea di fondo:Evitare goto per evitare goto è inutile.Ciò che vuoi veramente evitare è produrre codice illeggibile.Se tuo goto-laden è leggibile, quindi non c'è niente di sbagliato in esso.

Da goto rende difficile il ragionamento sul flusso del programma1 (aka.“codice spaghetti”), goto viene generalmente utilizzato solo per compensare le funzionalità mancanti:L'impiego di goto può effettivamente essere accettabile, ma solo se il linguaggio non offre una variante più strutturata per ottenere lo stesso obiettivo.Prendiamo l'esempio del dubbio:

La regola che utilizziamo con goto è che goto va bene per passare a un singolo punto di pulizia dell'uscita in una funzione.

Questo è vero, ma solo se il linguaggio non consente la gestione strutturata delle eccezioni con codice di pulizia (come RAII o finally), che svolge meglio lo stesso lavoro (poiché è creato appositamente per farlo), o quando c'è una buona ragione per non utilizzare la gestione strutturata delle eccezioni (ma non si verificherà mai questo caso se non a un livello molto basso).

Nella maggior parte delle altre lingue, l'unico uso accettabile di goto è uscire dai cicli nidificati.E anche in questo caso è quasi sempre meglio trasformare il circuito esterno in un proprio metodo e utilizzo return Invece.

Oltre a quello, goto è un segno che non si è pensato abbastanza a quel particolare pezzo di codice.


1 Lingue moderne che supportano goto implementare alcune restrizioni (es. goto potrebbe non entrare o uscire dalle funzioni) ma il problema fondamentalmente rimane lo stesso.

Per inciso, lo stesso vale ovviamente anche per altre caratteristiche linguistiche, in particolare per le eccezioni.E di solito esistono regole rigide per utilizzare queste funzionalità solo dove indicato, come la regola di non utilizzare eccezioni per controllare il flusso del programma non eccezionale.

Beh, c'è sempre una cosa peggiore di goto's;strano uso di altri operatori di programflow per evitare un goto:

Esempi:

    // 1
    try{
      ...
      throw NoErrorException;
      ...
    } catch (const NoErrorException& noe){
      // This is the worst
    } 


    // 2
    do {
      ...break; 
      ...break;
    } while (false);


    // 3
    for(int i = 0;...) { 
      bool restartOuter = false;
      for (int j = 0;...) {
        if (...)
          restartOuter = true;
      if (restartOuter) {
        i = -1;
      }
    }

etc
etc

In C# interruttore dichiarazione non consente la caduta.COSÌ vai a viene utilizzato per trasferire il controllo a un'etichetta specifica del quadro elettrico o al predefinito etichetta.

Per esempio:

switch(value)
{
  case 0:
    Console.Writeln("In case 0");
    goto case 1;
  case 1:
    Console.Writeln("In case 1");
    goto case 2;
  case 2:
    Console.Writeln("In case 2");
    goto default;
  default:
    Console.Writeln("In default");
    break;
}

Modificare:C'è un'eccezione alla regola "nessuna caduta".Il fall-through è consentito se un'istruzione case non ha codice.

#ifdef TONGUE_IN_CHEEK

Perl ha un goto che ti consente di implementare le chiamate di coda del povero.:-P

sub factorial {
    my ($n, $acc) = (@_, 1);
    return $acc if $n < 1;
    @_ = ($n - 1, $acc * $n);
    goto &factorial;
}

#endif

Ok, quindi questo non ha niente a che fare con C goto.Più seriamente, sono d'accordo con gli altri commenti sull'utilizzo goto per le pulizie o per l'implementazione Il dispositivo di Duff, o simili.Si tratta di usare, non di abusare.

(Lo stesso commento può applicarsi a longjmp, eccezioni, call/cc, e simili: hanno usi legittimi, ma se ne può facilmente abusare.Ad esempio, lanciando un'eccezione esclusivamente per sfuggire a una struttura di controllo profondamente annidata, in circostanze del tutto non eccezionali.)

Nel corso degli anni ho scritto più di qualche riga in linguaggio assembly.In definitiva, ogni linguaggio di alto livello si riduce a goto.Ok, chiamali "rami" o "salti" o qualsiasi altra cosa, ma sono goto.Qualcuno può scrivere un assembler goto-less?

Ora certo, puoi far notare a un programmatore Fortran, C o BASIC che scatenarsi con i goto è una ricetta per gli spaghetti alla bolognese.La risposta però non è evitarli, ma usarli con attenzione.

Un coltello può essere usato per preparare il cibo, liberare qualcuno o uccidere qualcuno.Facciamo a meno dei coltelli per paura di questi ultimi?Allo stesso modo vai a:usato con noncuranza ostacola, usato con attenzione aiuta.

Dare un'occhiata a Quando utilizzare Goto durante la programmazione in C:

Sebbene l'uso di goto sia quasi sempre una cattiva pratica di programmazione (sicuramente puoi trovare un modo migliore per fare XYZ), ci sono momenti in cui non è davvero una cattiva scelta.Alcuni potrebbero addirittura sostenere che, quando è utile, è la scelta migliore.

La maggior parte di ciò che ho da dire su goto si applica solo a C.Se stai usando C++, non c'è motivo valido per usare goto al posto delle eccezioni.In C, tuttavia, non hai la potenza di un meccanismo di gestione delle eccezioni, quindi se vuoi separare la gestione degli errori dal resto della logica del programma e vuoi evitare di riscrivere il codice di pulizia più volte nel codice, allora vai a può essere una buona scelta.

Cosa voglio dire?Potresti avere del codice simile a questo:

int big_function()
{
    /* do some work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* do some more work */
    if([error])
    {
        /* clean up*/
        return [error];
    }
    /* clean up*/
    return [success];
}

Questo va bene finché non ti rendi conto che devi modificare il codice di pulizia.Quindi devi passare e apportare 4 modifiche.Ora potresti decidere di incapsulare tutta la pulizia in un'unica funzione;non è una cattiva ideaMa significa che dovrai stare attento con i puntatori: se prevedi di liberare un puntatore nella tua funzione di pulizia, non c'è modo di impostarlo in modo che punti a NULL a meno che non passi un puntatore a un puntatore.In molti casi, non utilizzerai più quel puntatore comunque, quindi potrebbe non essere un grosso problema.D'altra parte, se aggiungi un nuovo puntatore, un handle di file o altro che necessita di pulizia, dovrai modificare nuovamente la funzione di pulizia;e quindi dovrai modificare gli argomenti di quella funzione.

Usando goto, sarà

int big_function()
{
    int ret_val = [success];
    /* do some work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
    /* do some more work */
    if([error])
    {
        ret_val = [error];
        goto end;
    }
end:
    /* clean up*/
    return ret_val;
}

Il vantaggio qui è che il codice seguente ha accesso a tutto ciò di cui avrà bisogno per eseguire la pulizia e sei riuscito a ridurre considerevolmente il numero di punti di modifica.Un altro vantaggio è che sei passato dall'avere più punti di uscita per la tua funzione a uno solo;non c'è alcuna possibilità che tu ritorni accidentalmente dalla funzione senza ripulire.

Inoltre, poiché goto viene utilizzato solo per passare a un singolo punto, non è come se si stesse creando una massa di codice spaghetti che salta avanti e indietro nel tentativo di simulare chiamate di funzione.Piuttosto, goto aiuta effettivamente a scrivere codice più strutturato.


In una parola, goto dovrebbe sempre essere usato con parsimonia e come ultima risorsa, ma c'è un tempo e un luogo per farlo.La domanda non dovrebbe essere "devi usarlo?" ma "è la scelta migliore" per usarlo.

Uno dei motivi per cui goto non è valido, oltre allo stile di codifica, è che puoi usarlo per creare sovrapposizione, Ma non nidificato cicli:

loop1:
  a
loop2:
  b
  if(cond1) goto loop1
  c
  if(cond2) goto loop2

Ciò creerebbe la struttura bizzarra, ma forse legale, del flusso di controllo in cui è possibile una sequenza come (a, b, c, b, a, b, a, b, ...), il che rende infelici gli hacker dei compilatori.Apparentemente ci sono una serie di trucchi di ottimizzazione intelligenti che si basano sul fatto che questo tipo di struttura non si verifichi.(Dovrei controllare la mia copia del libro del drago...) Il risultato di ciò potrebbe (usando alcuni compilatori) essere che altre ottimizzazioni non vengano eseguite per il codice che contiene gotoS.

Potrebbe essere utile se tu Sapere semplicemente, "oh, a proposito", sembra persuadere il compilatore a emettere codice più veloce.Personalmente, preferirei provare a spiegare al compilatore cosa è probabile e cosa no prima di usare un trucco come goto, ma probabilmente potrei anche provare goto prima di hackerare l'assemblatore.

Trovo divertente che alcune persone arrivino al punto di fornire un elenco di casi in cui goto è accettabile, dicendo che tutti gli altri usi sono inaccettabili.Pensi davvero di conoscere tutti i casi in cui goto è la scelta migliore per esprimere un algoritmo?

Per illustrare, ti darò un esempio che nessuno qui ha ancora mostrato:

Oggi stavo scrivendo il codice per inserire un elemento in una tabella hash.La tabella hash è una cache di calcoli precedenti che può essere sovrascritta a piacimento (influendo sulle prestazioni ma non sulla correttezza).

Ogni bucket della tabella hash ha 4 slot e ho una serie di criteri per decidere quale elemento sovrascrivere quando un bucket è pieno.In questo momento questo significa effettuare fino a tre passaggi attraverso un secchio, in questo modo:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

Ora, se non utilizzassi goto, come sarebbe questo codice?

Qualcosa come questo:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

Sarebbe sempre peggio se si aggiungessero più passaggi, mentre la versione con goto mantiene sempre lo stesso livello di indentazione ed evita l'uso di istruzioni if ​​spurie il cui risultato è implicito nell'esecuzione del ciclo precedente.

Quindi c'è un altro caso in cui goto rende il codice più pulito e più facile da scrivere e comprendere...Sono sicuro che ce ne sono molti altri, quindi non fingere di conoscere tutti i casi in cui goto è utile, disprezzando quelli buoni a cui non potresti pensare.

La regola che utilizziamo con goto è che goto va bene per passare a un singolo punto di pulizia dell'uscita in una funzione.Nelle funzioni veramente complesse allentiamo questa regola per consentire altri salti in avanti.In entrambi i casi stiamo evitando istruzioni if ​​profondamente annidate che spesso si verificano con il controllo del codice di errore, il che aiuta la leggibilità e la manutenzione.

La discussione più ponderata e approfondita sulle dichiarazioni goto, sui loro usi legittimi e sui costrutti alternativi che possono essere utilizzati al posto delle "dichiarazioni goto virtuose" ma di cui si può abusare con la stessa facilità delle dichiarazioni goto, è l'articolo di Donald Knuth "Programmazione strutturata con istruzioni goto", in Computing Surveys del dicembre 1974 (volume 6, n.4.pag.261 - 301).

Non sorprende che alcuni aspetti di questo documento vecchio di 39 anni siano datati:Aumenti di ordini di grandezza nella potenza di elaborazione rendono alcuni dei miglioramenti prestazionali di Knuth impercettibili per problemi di dimensioni moderate e da allora sono stati inventati nuovi costrutti del linguaggio di programmazione.(Ad esempio, i blocchi try-catch comprendono il Costrutto di Zahn, anche se sono usati raramente in questo modo.) Ma Knuth copre tutti gli aspetti dell'argomento e dovrebbe essere obbligatorio leggerlo prima che qualcuno riproponga nuovamente la questione.

In un modulo Perl, occasionalmente vorrete creare subroutine o chiusure al volo.Il fatto è che, una volta creata la subroutine, come ci si arriva?Potresti semplicemente chiamarlo, ma poi se la subroutine utilizza caller() non sarà così utile come potrebbe essere.È lì che goto &subroutine la variazione può essere utile.

Ecco un rapido esempio:

sub AUTOLOAD{
  my($self) = @_;
  my $name = $AUTOLOAD;
  $name =~ s/.*:://;

  *{$name} = my($sub) = sub{
    # the body of the closure
  }

  goto $sub;

  # nothing after the goto will ever be executed.
}

Puoi anche usare questa forma di goto per fornire una forma rudimentale di ottimizzazione delle chiamate in coda.

sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;

  $tally *= $n--;
  @_ = ($n,$tally);
  goto &factorial;
}

( In Perl 5 versione 16 sarebbe meglio scriverlo come goto __SUB__; )

C'è un modulo che importerà a tail modificatore e uno che verrà importato recur se non ti piace usare questo formato di goto.

use Sub::Call::Tail;
sub AUTOLOAD {
  ...
  tail &$sub( @_ );
}

use Sub::Call::Recur;
sub factorial($){
  my($n,$tally) = (@_,1);

  return $tally if $n <= 1;
  recur( $n-1, $tally * $n );
}

La maggior parte degli altri motivi per utilizzarlo goto sono fatti meglio con altre parole chiave.

Come redoscrivendo un po' di codice:

LABEL: ;
...
goto LABEL if $x;
{
  ...
  redo if $x;
}

Oppure andare al last di un po' di codice da più posti:

goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
  last if $x;
  ...
  last if $y
  ...
}

Se sì, perché?

C non ha interruzioni multilivello/etichettate e non tutti i flussi di controllo possono essere facilmente modellati con l'iterazione e le primitive decisionali di C.i goto fanno molto per correggere questi difetti.

A volte è più chiaro usare una variabile flag di qualche tipo per effettuare una sorta di interruzione pseudo-multi-livello, ma non è sempre superiore a goto (almeno goto permette di determinare facilmente dove va a finire il controllo, a differenza di una variabile flag ), e talvolta semplicemente non vuoi pagare il prezzo delle prestazioni di bandiere/altre contorsioni per evitare il goto.

libavcodec è un pezzo di codice sensibile alle prestazioni.L'espressione diretta del flusso di controllo è probabilmente una priorità, perché tenderà a funzionare meglio.

Meno male che nessuno ha mai implementato l'affermazione "COME FROM"....

Trovo che l'uso del fare{} mentre(falso) sia assolutamente rivoltante.È concepibile che possa convincermi che sia necessario in qualche caso strano, ma mai che si tratti di un codice pulito e sensato.

Se è necessario eseguire un ciclo di questo tipo, perché non rendere esplicita la dipendenza dalla variabile flag?

for (stepfailed=0 ; ! stepfailed ; /*empty*/)

Il GOTO può essere utilizzato, ovviamente, ma c'è una cosa più importante dello stile del codice, o se il codice è o meno leggibile, che devi tenere a mente quando lo usi: il codice all'interno potrebbe non essere così robusto come pensi.

Ad esempio, guarda i seguenti due frammenti di codice:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

Un codice equivalente con GOTO

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

La prima cosa che pensiamo è che il risultato di entrambi i bit di codice sarà "Valore di A:0" (supponiamo ovviamente un'esecuzione senza parallelismo)

Non è corretto:nel primo campione A sarà sempre 0, ma nel secondo campione (con l'istruzione GOTO) A potrebbe non essere 0.Perché?

Il motivo è perché da un altro punto del programma posso inserire un file GOTO FINAL senza controllare il valore di A.

Questo esempio è molto ovvio, ma man mano che i programmi diventano più complicati, aumenta la difficoltà di vedere questo tipo di cose.

Materiale correlato può essere trovato nel famoso articolo di Mr.Dijkstra "Un caso contro la dichiarazione GO TO"

1) L'uso più comune di goto che conosco è l'emulazione della gestione delle eccezioni in linguaggi che non la offrono, in particolare in C.(Il codice fornito da Nuclear sopra è proprio questo.) Guarda il codice sorgente di Linux e vedrai un fantastilione di goto usati in questo modo;c'erano circa 100.000 goto nel codice Linux secondo un rapido sondaggio condotto nel 2013: http://blog.regehr.org/archives/894.L'utilizzo di Goto è menzionato anche nella guida allo stile di codifica di Linux: https://www.kernel.org/doc/Documentation/CodingStyle.Proprio come la programmazione orientata agli oggetti viene emulata utilizzando strutture popolate con puntatori a funzione, goto trova il suo posto nella programmazione C.Allora chi ha ragione:Dijkstra o Linus (e tutti i programmatori del kernel Linux)?È teoria contropratica sostanzialmente.

Esiste tuttavia il solito problema di non avere supporto a livello di compilatore e controlli per costrutti/modelli comuni:è più facile usarli in modo sbagliato e introdurre bug senza controlli in fase di compilazione.Windows e Visual C++ ma in modalità C offrono la gestione delle eccezioni tramite SEH/VEH proprio per questo motivo:le eccezioni sono utili anche al di fuori dei linguaggi OOP, ad es.in un linguaggio procedurale.Ma il compilatore non può sempre salvarvi, anche se offre supporto sintattico per le eccezioni nel linguaggio.Consideriamo come esempio di quest'ultimo caso il famoso bug Apple SSL "goto fail", che duplicava semplicemente un goto con conseguenze disastrose (https://www.imperialviolet.org/2014/02/22/applebug.html):

if (something())
  goto fail;
  goto fail; // copypasta bug
printf("Never reached\n");
fail:
  // control jumps here

Puoi avere esattamente lo stesso bug usando le eccezioni supportate dal compilatore, ad es.in C++:

struct Fail {};

try {
  if (something())
    throw Fail();
    throw Fail(); // copypasta bug
  printf("Never reached\n");
}
catch (Fail&) {
  // control jumps here
}

Ma entrambe le varianti del bug possono essere evitate se il compilatore analizza e avvisa del codice irraggiungibile.Ad esempio, la compilazione con Visual C++ al livello di avviso /W4 rileva il bug in entrambi i casi.Java, ad esempio, vieta il codice irraggiungibile (dove può trovarlo!) per una buona ragione:è probabile che si tratti di un bug nel codice medio di Joe.Finché il costrutto goto non consente obiettivi che il compilatore non riesce a capire facilmente, come goto verso indirizzi calcolati(**), non è più difficile per il compilatore trovare codice irraggiungibile all'interno di una funzione con goto rispetto all'utilizzo di Dijkstra -codice approvato.

(**) Nota:In alcune versioni di Basic è possibile andare a numeri di riga calcolati, ad es.GOTO 10*x dove x è una variabile.In modo piuttosto confuso, in Fortran "computed goto" si riferisce a un costrutto equivalente a un'istruzione switch in C.Lo standard C non consente goto calcolati nel linguaggio, ma solo goto a etichette dichiarate staticamente/sintatticamente.GNU C ha tuttavia un'estensione per ottenere l'indirizzo di un'etichetta (l'operatore unario, prefisso &&) e consente anche un goto a una variabile di tipo void*.Vedere https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html per ulteriori informazioni su questo oscuro sottoargomento.Il resto di questo post non riguarda quell'oscura funzionalità GNU C.

Norma C (es.non calcolato) i goto non sono solitamente il motivo per cui non è possibile trovare codice irraggiungibile in fase di compilazione.Il solito motivo è un codice logico come il seguente.Dato

int computation1() {
  return 1;
}

int computation2() {
  return computation1();
}

È altrettanto difficile per un compilatore trovare codice irraggiungibile in uno qualsiasi dei seguenti 3 costrutti:

void tough1() {
  if (computation1() != computation2())
    printf("Unreachable\n");
}

void tough2() {
  if (computation1() == computation2())
    goto out;
  printf("Unreachable\n");
out:;
}

struct Out{};

void tough3() {
  try {
    if (computation1() == computation2())
      throw Out();
    printf("Unreachable\n");
  }
  catch (Out&) {
  }
}

(Scusate il mio stile di codifica relativo alle parentesi graffe, ma ho cercato di mantenere gli esempi il più compatti possibile.)

Visual C++ /W4 (anche con /Ox) non riesce a trovare codice irraggiungibile in nessuno di questi e, come probabilmente saprai, il problema di trovare codice irraggiungibile è in generale indecidibile.(Se non mi credi a questo proposito: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)

Come problema correlato, il C goto può essere utilizzato per emulare eccezioni solo all'interno del corpo di una funzione.La libreria C standard offre una coppia di funzioni setjmp() e longjmp() per emulare uscite/eccezioni non locali, ma queste presentano alcuni seri inconvenienti rispetto a ciò che offrono altri linguaggi.L'articolo di Wikipedia http://en.wikipedia.org/wiki/Setjmp.h spiega abbastanza bene quest'ultimo problema.Questa coppia di funzioni funziona anche su Windows (http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx), ma lì quasi nessuno li usa perché SEH/VEH è superiore.Anche su Unix, penso che setjmp e longjmp siano usati molto raramente.

2) Penso che il secondo utilizzo più comune di goto in C sia l'implementazione dell'interruzione multilivello o della continuazione multilivello, che è anche un caso d'uso abbastanza incontrovertibile.Ricordiamo che Java non consente l'etichetta goto, ma consente l'etichetta break o l'etichetta continue.Secondo http://www.oracle.com/technetwork/java/simple-142616.html, questo è in realtà il caso d'uso più comune dei goto in C (90% dicono), ma nella mia esperienza soggettiva, il codice di sistema tende a utilizzare più spesso i goto per la gestione degli errori.Forse nel codice scientifico o dove il sistema operativo offre la gestione delle eccezioni (Windows), le uscite multilivello sono il caso d'uso dominante.In realtà non forniscono alcun dettaglio sul contesto del loro sondaggio.

Modificato per aggiungere:si scopre che questi due modelli d'uso si trovano nel libro C di Kernighan e Ritchie, intorno a pagina 60 (a seconda dell'edizione).Un'altra cosa degna di nota è che entrambi i casi d'uso coinvolgono solo i goto in avanti.E si scopre che l'edizione MISRA C 2012 (a differenza dell'edizione 2004) ora consente i goto, purché siano solo diretti.

In Perl, uso di un'etichetta per "goto" da un ciclo - utilizzando un'istruzione "last", che è simile a break.

Ciò consente un migliore controllo sui cicli nidificati.

Il tradizionale goto etichetta è supportato, ma non sono sicuro che ci siano molti casi in cui questo è l'unico modo per ottenere ciò che desideri: subroutine e loop dovrebbero essere sufficienti nella maggior parte dei casi.

Il problema con "goto" e l'argomento più importante del movimento "programmazione goto-less" è che se lo usi troppo frequentemente il tuo codice, sebbene possa comportarsi correttamente, diventa illeggibile, non manutenibile, non revisionabile ecc.Nel 99,99% dei casi 'goto' porta allo spaghetti code.Personalmente, non riesco a pensare ad alcuna buona ragione per cui dovrei usare "goto".

Anche Edsger Dijkstra, un informatico che ha dato importanti contributi in questo campo, era famoso per aver criticato l'uso di GoTo.C'è un breve articolo sulla sua argomentazione su Wikipedia.

Utilizzo goto nel seguente caso:quando necessario per tornare da funzioni in posti diversi, e prima di tornare è necessario eseguire alcune operazioni di deinizializzazione:

versione non goto:

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

vai alla versione:

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

La seconda versione rende più semplice, quando è necessario modificare qualcosa nelle istruzioni di deallocazione (ognuna viene utilizzata una volta nel codice), e riduce la possibilità di saltarne qualcuna, quando si aggiunge un nuovo ramo.Spostarli in una funzione non aiuterà in questo caso, perché la deallocazione può essere eseguita a diversi "livelli".

Alcuni dicono che non c'è motivo di andare a in C++.Alcuni dicono che nel 99% dei casi esistono alternative migliori. Questo non è un ragionamento, solo impressioni irrazionali. Ecco un esempio concreto in cui goto porta a un bel codice, qualcosa come un ciclo do- while potenziato:

int i;

PROMPT_INSERT_NUMBER:
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    goto PROMPT_INSERT_NUMBER;          
  }

std::cout << "your number is " << i;

Confrontalo con il codice goto-free:

int i;

bool loop;
do {
  loop = false;
  std::cout << "insert number: ";
  std::cin >> i;
  if(std::cin.fail()) {
    std::cin.clear();
    std::cin.ignore(1000,'\n');
    loop = true;          
  }
} while(loop);

std::cout << "your number is " << i;

Vedo queste differenze:

  • nidificato {} è necessario il blocco (anche se do {...} while sembra più familiare)
  • extra loop è necessaria la variabile, utilizzata in quattro posti
  • ci vuole più tempo per leggere e comprendere il lavoro con il loop
  • IL loop non contiene dati, controlla solo il flusso di esecuzione, che è meno comprensibile di una semplice etichetta

C'è un altro esempio

void sort(int* array, int length) {
SORT:
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    goto SORT; // it is very easy to understand this code, right?
  }
}

Ora liberiamoci del "malvagio" goto:

void sort(int* array, int length) {
  bool seemslegit;
  do {
    seemslegit = true;
    for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
      swap(data[i], data[i+1]);
      seemslegit = false;
    }
  } while(!seemslegit);
}

Vedete, è lo stesso tipo di utilizzo di goto, è un modello ben strutturato e non è il goto inoltrato che molti promuovono come l'unico modo consigliato.Sicuramente vorrai evitare un codice "intelligente" come questo:

void sort(int* array, int length) {
  for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
    swap(data[i], data[i+1]);
    i = -1; // it works, but WTF on the first glance
  }
}

Il punto è che goto può essere facilmente utilizzato in modo improprio, ma goto stesso non è da biasimare.Tieni presente che label ha un ambito di funzione in C++, quindi non inquina l'ambito globale come nell'assembly puro, in cui anelli sovrapposti hanno il loro posto e sono molto comuni, come nel seguente codice per 8051, dove il display a 7 segmenti è collegato a P1.Il programma ripete il segmento del fulmine attorno a:

; P1 states loops
; 11111110 <-
; 11111101  |
; 11111011  |
; 11110111  |
; 11101111  |
; 11011111  |
; |_________|

init_roll_state:
    MOV P1,#11111110b
    ACALL delay
next_roll_state:
    MOV A,P1
    RL A
    MOV P1,A
    ACALL delay
    JNB P1.5, init_roll_state
    SJMP next_roll_state

C'è un altro vantaggio:goto può fungere da cicli con nome, condizioni e altri flussi:

if(valid) {
  do { // while(loop)

// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket

  } while(loop);
} // if(valid)

Oppure puoi usare l'equivalente goto con rientro, quindi non hai bisogno di commenti se scegli saggiamente il nome dell'etichetta:

if(!valid) goto NOTVALID;
  LOOPBACK:

// more than one page of code here

  if(loop) goto LOOPBACK;
NOTVALID:;
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top