2009-03-31 12 views
6

Sì, due costrutti odiati combinati. È così brutto come sembra o può essere visto come un buon modo per controllare l'utilizzo di goto e anche fornire una ragionevole strategia di pulizia?Macro di controllo del flusso con 'goto'

Al lavoro abbiamo discusso se consentire o meno il goto nel nostro standard di codifica. In generale nessuno voleva consentire l'uso gratuito di goto, ma alcuni erano positivi nell'usarlo per i salti di pulizia. Come in questo codice:

void func() 
{ 
    char* p1 = malloc(16); 
    if(!p1) 
     goto cleanup; 

    char* p2 = malloc(16); 
    if(!p2) 
     goto cleanup; 

goto norm_cleanup; 

err_cleanup: 

    if(p1) 
     free(p1); 

    if(p2) 
     free(p2); 

norm_cleanup: 
} 

Il beneficio abovious di tale utilizzo è che non c'è bisogno di finire con questo codice:

void func() 
{ 
    char* p1 = malloc(16); 
    if(!p1){ 
     return; 
    } 

    char* p2 = malloc(16); 
    if(!p2){ 
     free(p1); 
     return; 
    } 

    char* p3 = malloc(16); 
    if(!p3){ 
     free(p1); 
     free(p2); 
     return; 
    } 
} 

Soprattutto in funzioni di costruzione-come con molte allocazioni questo può a volte cresce molto male, non meno importante quando qualcuno deve inserire qualcosa nel mezzo.

Quindi, per poter utilizzare goto, ma ancora chiaramente isolarlo dall'essere utilizzato liberamente, è stata creata una serie di macro di controllo del flusso per la gestione dell'attività. Sembra qualcosa di simile (semplificato):

#define FAIL_SECTION_BEGIN int exit_code[GUID] = 0; 
#define FAIL_SECTION_DO_EXIT_IF(cond, exitcode) if(cond){exit_code[GUID] = exitcode; goto exit_label[GUID];} 
#define FAIL_SECTION_ERROR_EXIT(code) exit_label[GUID]: if(exit_code[GUID]) int code = exit_code[GUID];else goto end_label[GUID] 
#define FAIL_SECTION_END end_label[GUID]: 

possiamo usare questa come segue:

int func() 
{ 
    char* p1 = NULL; 
    char* p2 = NULL; 
    char* p3 = NULL; 

    FAIL_SECTION_BEGIN 
    { 
     p1 = malloc(16); 
     FAIL_SECTION_DO_EXIT_IF(!p1, -1); 

     p2 = malloc(16); 
     FAIL_SECTION_DO_EXIT_IF(!p2, -1); 

     p3 = malloc(16); 
     FAIL_SECTION_DO_EXIT_IF(!p3, -1); 
    } 
    FAIL_SECTION_ERROR_EXIT(code) 
    { 
     if(p3) 
     free(p3); 

     if(p2) 
     free(p2); 

     if(p1) 
     free(p1); 

     return code; 
    } 
    FAIL_SECTION_END 

    return 0; 

Sembra bello, e viene fornito con molti vantaggi, ma ci sono degli svantaggi che dovremmo pensare a prima di lanciare questo fuori nello sviluppo? Dopo tutto controlla molto il flusso e goto: ish. Entrambi sono scoraggiati. Quali sono gli argomenti per scoraggiarli in questo caso?

Grazie.

+0

Come Martin Fido sottolinea nella sua risposta, nel frammento di codice finale i puntatori p1, p2 e p3 sono tutti fuori dalla sezione di gestione degli errori, e nel primo frammento di codice p2 conterrà i dati di spazzatura (se si compila a tutti - non sono sicuro delle regole C). –

+0

@j: Sono sicuro che questo codice non passerà nemmeno attraverso il compilatore. Grazie per il suggerimento, ma devi aver perso l'intero punto della domanda. – sharkin

+0

@ R.A: cosa ti fa pensare che mi sia mancato il punto? La non-compilabilità era un problema secondario che pensavo valesse la pena di sottolineare in un commento. –

risposta

11

La gestione degli errori è uno dei rari casi in cui è goto non così male.

Ma se dovessi mantenere quel codice sarei molto turbato dal fatto che goto sono nascosti da macro.

Quindi in questo caso goto è OK per me ma non macro.

+0

la gestione degli errori non dovrebbe essere una situazione rara: P – quinmars

+0

la situazione rara non è "gestione degli errori", è "goto usage". – mouviciel

+0

Non mi interessa usare goto all'interno di una macro e farlo di tanto in tanto, MA nascondere l'etichetta saltata dentro la macro è un assoluto no-no. Cioè non ok: "#define CHECK_RESULT (res) if (res == 0) goto cleanup;", ok: "#define CHECK_RESULT (res, label) if (res == 0) goto label;". In questo modo puoi capire che la macro salta (ad es. "Res = something(); CHECK_RESULT (res, cleanup); ... cleanup: ..."). – hlovdal

7

L'utilizzo di goto per passare a un gestore di errori comune/sequenza di pulizia/uscita è assolutamente soddisfacente.

+0

Tuttavia, vorrei escludere i macro. Gli occhi del tuo programmatore sono addestrati per if() s. Queste macro appaiono relativamente raramente e sono quindi molto più difficili da leggere. –

7

Questo codice:

void func() 
{ 
    char* p1 = malloc(16); 
    if(!p1) 
     goto cleanup; 

    char* p2 = malloc(16); 
    if(!p2) 
     goto cleanup; 

cleanup: 

    if(p1) 
     free(p1); 

    if(p2) 
     free(p2); 
} 

può essere legalmente scritto come:

void func() 
{ 
    char* p1 = malloc(16); 
    char* p2 = malloc(16); 

    free(p1); 
    free(p2); 
} 

se le allocazioni di memoria successo.

Funziona perché free() non esegue nulla se ha passato un puntatore NULL. È possibile utilizzare lo stesso linguaggio per la creazione di proprie API per allocare e altre risorse gratuite:

// return handle to new Foo resource, or 0 if allocation failed 
FOO_HANDLE AllocFoo(); 

// release Foo indicated by handle, - do nothing if handle is 0 
void ReleaseFoo(FOO_HANDLE h); 

Progettare le API come questo può semplificare notevolmente la gestione delle risorse.

+0

Sì, mi dispiace, ho dimenticato un po 'di codice. Aggiornata la domanda. – sharkin

+0

Tuttavia è malloc specifico, che ne dite di chiamare altre risorse che assegnano le funzioni? Non lo sapevo comunque, grazie – shodanex

+0

Wrong, quel codice è specifico per C99. – sharkin

1

Il primo esempio mi sembra molto più leggibile della versione macroizzata. E mouviciel ha detto che molto meglio di quanto ho fatto

2

Il termine "programmazione strutturata", che sappiamo tutti come la cosa anti-goto originariamente iniziato e sviluppato come un mazzo di codifica modelli con goto di (o JMP di). Questi modelli sono stati chiamati modelli while e if, tra gli altri.

Quindi, se si utilizza goto, usarli in modo strutturato. Questo limita il danno. E quelle macro sembrano un approccio ragionevole.

3

La pulizia con goto è un linguaggio C comune ed è used in Linux kernel *.

** Forse opinione di Linus' non è il miglior esempio di un buon argomento, ma lo fa mostrare goto essere utilizzato in un progetto di scala relativamente grande. *

3

Se il primo malloc non funziona, è necessario pulire sia p1 sia p2. A causa del goto, p2 non è inizializzato e potrebbe puntare a qualcosa. L'ho eseguito rapidamente con gcc per verificare e il tentativo di liberare (p2) avrebbe effettivamente causato un errore di seg.

Nel tuo ultimo esempio le variabili sono circoscritte all'interno delle parentesi (cioè esistono solo nel blocco FAIL_SECTION_BEGIN).

Supponendo che il codice funzioni senza le parentesi graffe, occorre ancora inizializzare tutti i puntatori su NULL prima di FAIL_SECTION_BEGIN per evitare errori del segmento.

Non ho nulla contro goto e le macro, ma io preferisco l'idea di Neil Butterworth ..

void func(void) 
{ 
    void *p1 = malloc(16); 
    void *p2 = malloc(16); 
    void *p3 = malloc(16); 

    if (!p1 || !p2 || !p3) goto cleanup; 

    /* ... */ 

cleanup: 
    if (p1) free(p1); 
    if (p2) free(p2); 
    if (p3) free(p3); 
} 

O se è più opportuno ..

void func(void) 
{ 
    void *p1 = NULL; 
    void *p2 = NULL; 
    void *p3 = NULL; 

    p1 = malloc(16); 
    if (!p1) goto cleanup; 

    p2 = malloc(16); 
    if (!p2) goto cleanup; 

    p3 = malloc(16); 
    if (!p3) goto cleanup; 

    /* ... */ 

cleanup: 
    if (p1) free(p1); 
    if (p2) free(p2); 
    if (p3) free(p3); 
} 
+0

+1. Ottimo punto su come giocare con le variabili che "non esistono ancora" nella sezione di pulizia. –

+0

Ovviamente è un errore di battitura, e ovviamente non ho provato a compilarlo (i puntatori sarebbero inutilizzabili nella sezione di uscita). Ovviamente tali dettagli non sono davvero l'essenza della domanda in questione. – sharkin

+0

@ R.A: No, non sono l'essenza, ma offuscano il punto che stai cercando di fare.Quando pubblichi frammenti di codice, è semplice cortesia assicurarti che vengano compilati e funzionanti come previsto. –

2

Il codice originale sarebbe trarre vantaggio dall'utilizzo di più istruzioni return - non è necessario saltare il codice di cancellazione dell'errore. Inoltre, normalmente è necessario che lo spazio allocato venga rilasciato anche in caso di ritorno ordinario, altrimenti si perde memoria. E puoi riscrivere l'esempio senza goto se stai attento. Questo è un caso dove si può utilmente dichiarare variabili prima altrimenti necessaria:

void func() 
{ 
    char *p1 = 0; 
    char *p2 = 0; 
    char *p3 = 0; 

    if ((p1 = malloc(16)) != 0 && 
     (p2 = malloc(16)) != 0 && 
     (p3 = malloc(16)) != 0) 
    { 
     // Use p1, p2, p3 ... 
    } 
    free(p1); 
    free(p2); 
    free(p3); 
} 

Quando ci sono importi non banali lavoro dopo ogni operazione di allocazione, allora si può utilizzare un'etichetta prima del primo dei free() operazioni, e un goto è OK - la gestione degli errori è il motivo principale per l'utilizzo di goto in questi giorni, e qualsiasi altra cosa è in qualche modo discutibile.

Mi prendo cura di un codice che dispone di macro con istruzioni goto incorporate. È confuso al primo incontro vedere un'etichetta "non referenziata" dal codice visibile, che tuttavia non può essere rimossa. Preferisco evitare tali pratiche. I macro sono OK quando non ho bisogno di sapere cosa fanno - lo fanno e basta. I macro non sono così OK quando devi sapere cosa si espandono per usarli con precisione. Se non nascondono informazioni da me, sono più di una seccatura che un aiuto.

Illustrazione - nomi mascherati per proteggere il colpevole:

#define rerrcheck if (currval != &localval && globvar->currtub &&   \ 
        globvar->currtub->te_flags & TE_ABORT)     \ 
        { if (globvar->currtub->te_state)      \ 
         globvar->currtub->te_state->ts_flags |= TS_FAILED;\ 
         else             \ 
         delete_tub_name(globvar->currtub->te_name);  \ 
         goto failure;          \ 
        } 


#define rgetunsigned(b) {if (_iincnt>=2) \ 
          {_iinptr+=2;_iincnt-=2;b = ldunsigned(_iinptr-2);} \ 
         else {b = _igetunsigned(); rerrcheck}} 

Ci sono diverse decine di varianti rgetunsigned() che sono in qualche modo simile - diverse dimensioni e diverse funzioni del caricatore.

Un luogo in cui vengono utilizzati questi contiene questo loop - in un blocco più grande di codice in un singolo caso di un grande interruttore con alcune piccole e alcuni grandi blocchi di codice (non particolarmente ben strutturata):

 for (i = 0 ; i < no_of_rows; i++) 
      { 
      row_t *tmprow = &val->v_coll.cl_typeinfo->clt_rows[i]; 

      rgetint(tmprow->seqno); 
      rgetint(tmprow->level_no); 
      rgetint(tmprow->parent_no); 
      rgetint(tmprow->fieldnmlen); 
      rgetpbuf(tmprow->fieldname, IDENTSIZE); 
      rgetint(tmprow->field_no); 
      rgetint(tmprow->type); 
      rgetint(tmprow->length); 
      rgetlong(tmprow->xid); 
      rgetint(tmprow->flags); 
      rgetint(tmprow->xtype_nm_len); 
      rgetpbuf(tmprow->xtype_name, IDENTSIZE); 
      rgetint(tmprow->xtype_owner_len); 
      rgetpbuf(tmprow->xtype_owner_name, IDENTSIZE); 
      rgetpbuf(tmprow->xtype_owner_name, 
        tmprow->xtype_owner_len); 
      rgetint(tmprow->alignment); 
      rgetlong(tmprow->sourcetype); 
      } 

Non è ovvio che il codice ci sia allacciato con le istruzioni goto! E chiaramente, una piena esegesi dei peccati del codice da cui proviene durerebbe tutto il giorno - sono molti e variegati.

0
#define malloc_or_die(size) if(malloc(size) == NULL) exit(1) 

Non è che si può davvero recuperare da falliti malloc di meno che non si dispone di un software vale la pena scrivere un sistema di transazione per, se lo fai, aggiungi roll back codice malloc_or_die.

Per un esempio reale di buon utilizzo di goto, controllare il codice di invio dell'analisi che utilizza il goto calcolato.

+0

Vorrei che potessi pubblicare un link ad alcune ulteriori informazioni su "goto calcolato". Sembra interessante. -1 per la tua opinione sull'assenza di allocazione della memoria. – sharkin

+0

Dal manutentore di pulseaudio: "Sui sistemi moderni è diventato chiaro che per il normale software di userspace solo un malloc() di annullamento ha senso [...]" http://article.gmane.org/gmane.comp.audio. jackit/19998 – CesarB

Problemi correlati