Prova le dichiarazioni catch in C


101

Oggi stavo pensando ai blocchi try / catch esistenti in altre lingue. Ho cercato su Google per un po 'ma senza risultati. Da quello che so, non esiste una cosa come try / catch in C. Tuttavia, c'è un modo per "simularli"?
Certo, ci sono assert e altri trucchi ma niente come try / catch, che catturano anche l'eccezione sollevata. Grazie


3
I meccanismi simili alle eccezioni non saranno generalmente utili senza un meccanismo per liberare automaticamente le risorse quando lo stack viene svolto. C ++ utilizza RAII; Java, C #, Python, ecc. Usano i garbage collector. (E nota che i garbage collector liberano solo memoria. Per liberare automaticamente altri tipi di risorse, aggiungono anche cose come finalizzatori o gestori di contesto ...)
jamesdlin

@jamesdlin, perché non potremmo fare RAII con C?
Pacerier

1
@Pacerier RAII richiede la chiamata automatica delle funzioni quando gli oggetti vengono distrutti (cioè i distruttori). Come proponi di farlo in C?
jamesdlin

Risposte:


90

C di per sé non supporta le eccezioni ma puoi simularle fino a un certo punto con setjmpe longjmpchiama.

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened here\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjmp(s_jumpBuffer, 42);
}

Questo sito web ha un bel tutorial su come simulare eccezioni con setjmpelongjmp


1
soluzione fantastica! questa soluzione è incrociata? Ha funzionato per me su MSVC2012 ma non nel compilatore MacOSX Clang.
mannysz

1
indovinami: pensavo che le clausole try catch ti permettessero di catturare le eccezioni (come la divisione per zero). Questa funzione sembra permetterti solo di catturare le eccezioni che ti lanci. Le vere eccezioni non vengono generate chiamando longjmp, giusto? Se uso questo codice per fare qualcosa di simile try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; non funzionerà bene?
Sam

Devide by zero non è nemmeno un'eccezione in C ++, per gestirlo devi controllare che il divisore non sia zero e gestirlo o gestire il SIGFPE che viene lanciato quando esegui una formula devide by zero.
James

25

Si utilizza goto in C per situazioni di gestione degli errori simili.
Questo è l'equivalente più vicino delle eccezioni che puoi ottenere in C.


3
@JensGustedt Questo è esattamente ciò per cui goto è attualmente usato molto spesso ed è un esempio in cui ha senso (setjmp / ljmp è un'alternativa migliore, ma label + goto è tipicamente usato di più).
Tomas Pruzina

1
@AoeAoe, probabilmente gotoè più utilizzato per la gestione degli errori, ma allora? La domanda non riguarda la gestione degli errori in quanto tale, ma esplicitamente gli equivalenti try / catch. gotonon è un equivalente per try / catch poiché è limitato alla stessa funzione.
Jens Gustedt

@ JensGustedt Ho reagito all'odio / paura di goto e delle persone che lo usano (i miei insegnanti mi hanno raccontato storie spaventose di utilizzo di goto anche all'università). [OT] L'unica cosa che è davvero, davvero rischiosa e la cosa "torbida" di goto è "andare indietro", ma l'ho visto in Linux VFS (git blame guy ha giurato che era critico per le prestazioni-vantaggioso).
Tomas Pruzina

Vedere le fonti di systemctl per gli usi legittimi di gotocome meccanismo di prova / cattura utilizzato in una fonte moderna, ampiamente accettata e sottoposta a revisione paritaria. Cerca gotoun equivalente "lancio" e finishun equivalente "cattura".
Stewart

13

Ok, non ho potuto resistere a rispondere a questo. Lasciatemi innanzitutto dire che non penso sia una buona idea simulare questo in C in quanto è davvero un concetto estraneo a C.

Possiamo usare abusare del preprocessore e delle variabili dello stack locale per dare l'uso di una versione limitata di C ++ try / throw / catch.

Versione 1 (lanci di ambito locale)

#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

La versione 1 è solo un lancio locale (non può lasciare l'ambito della funzione). Si basa sulla capacità di C99 di dichiarare variabili nel codice (dovrebbe funzionare in C89 se la prova è la prima cosa nella funzione).

Questa funzione crea solo una var locale in modo che sappia se c'è stato un errore e usa un goto per saltare al blocco catch.

Per esempio:

#include <stdio.h>
#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

int main(void)
{
    try
    {
        printf("One\n");
        throw();
        printf("Two\n");
    }
    catch(...)
    {
        printf("Error\n");
    }
    return 0;
}

Questo funziona in qualcosa di simile:

int main(void)
{
    bool HadError=false;
    {
        printf("One\n");
        HadError=true;
        goto ExitJmp;
        printf("Two\n");
    }
ExitJmp:
    if(HadError)
    {
        printf("Error\n");
    }
    return 0;
}

Versione 2 (scope jumping)

#include <stdbool.h>
#include <setjmp.h>

jmp_buf *g__ActiveBuf;

#define try jmp_buf __LocalJmpBuff;jmp_buf *__OldActiveBuf=g__ActiveBuf;bool __WasThrown=false;g__ActiveBuf=&__LocalJmpBuff;if(setjmp(__LocalJmpBuff)){__WasThrown=true;}else
#define catch(x) g__ActiveBuf=__OldActiveBuf;if(__WasThrown)
#define throw(x) longjmp(*g__ActiveBuf,1);

La versione 2 è molto più complessa ma fondamentalmente funziona allo stesso modo. Utilizza un salto in lungo dalla funzione corrente al blocco try. Il blocco try utilizza quindi un if / else per saltare il blocco di codice al blocco catch che controlla la variabile locale per vedere se dovrebbe catturare.

L'esempio si è espanso di nuovo:

jmp_buf *g_ActiveBuf;

int main(void)
{
    jmp_buf LocalJmpBuff;
    jmp_buf *OldActiveBuf=g_ActiveBuf;
    bool WasThrown=false;
    g_ActiveBuf=&LocalJmpBuff;

    if(setjmp(LocalJmpBuff))
    {
        WasThrown=true;
    }
    else
    {
        printf("One\n");
        longjmp(*g_ActiveBuf,1);
        printf("Two\n");
    }
    g_ActiveBuf=OldActiveBuf;
    if(WasThrown)
    {
        printf("Error\n");
    }
    return 0;
}

Questo utilizza un puntatore globale in modo che longjmp () sappia quale try è stata eseguita l'ultima volta. Stiamo usando abusando dello stack in modo che le funzioni figlie possano anche avere un blocco try / catch.

L'utilizzo di questo codice ha una serie di aspetti negativi (ma è un divertente esercizio mentale):

  • Non libererà la memoria allocata poiché non vengono chiamati decostruttori.
  • Non puoi avere più di 1 tentativo / cattura in un ambito (nessun annidamento)
  • Non puoi effettivamente lanciare eccezioni o altri dati come in C ++
  • Non è assolutamente sicuro per i thread
  • Stai configurando altri programmatori per il fallimento perché probabilmente non noteranno l'hack e proveranno a usarli come blocchi try / catch in C ++.

belle soluzioni alternative.
HaseeB Mir

la versione 1 è una buona idea, ma quella variabile __HadError dovrebbe essere reimpostata o definita come ambito. Altrimenti non sarai in grado di utilizzare più di un tentativo di cattura nello stesso blocco. Forse usa una funzione globale come bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}. Ma anche la variabile locale verrebbe ridefinita, quindi le cose sfuggono un po 'di mano.
flamewave000

Sì, è limitato a un tentativo di cattura nella stessa funzione. Un problema più grande della variabile tuttavia è l'etichetta in quanto non è possibile avere etichette duplicate nella stessa funzione.
Paul Hutchinson,

10

In C99, è possibile utilizzare setjmp/ longjmpper il flusso di controllo non locale.

All'interno di un unico ambito, il modello di codifica generico e strutturato per C in presenza di più allocazioni di risorse e più uscite utilizza goto, come in questo esempio . Questo è simile a come C ++ implementa le chiamate dei distruttori di oggetti automatici sotto il cofano, e se ti attieni a questo diligentemente, dovrebbe consentirti un certo grado di pulizia anche in funzioni complesse.


5

Mentre alcune delle altre risposte hanno coperto i casi semplici utilizzando setjmpe longjmp, in un'applicazione reale ci sono due preoccupazioni che contano davvero.

  1. Annidamento di blocchi try / catch. Utilizzando una singola variabile globale per il tuo jmp_buf, queste non funzioneranno.
  2. Filettatura. Una singola variabile globale per te jmp_bufcauserà tutti i tipi di dolore in questa situazione.

La soluzione a questi è mantenere uno stack locale del thread jmp_bufche viene aggiornato man mano che si procede. (Penso che questo sia ciò che lua usa internamente).

Quindi invece di questo (dalla fantastica risposta di JaredPar)

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjump(s_jumpBuffer, 42);
}

Useresti qualcosa come:

#define MAX_EXCEPTION_DEPTH 10;
struct exception_state {
  jmp_buf s_jumpBuffer[MAX_EXCEPTION_DEPTH];
  int current_depth;
};

int try_point(struct exception_state * state) {
  if(current_depth==MAX_EXCEPTION_DEPTH) {
     abort();
  }
  int ok = setjmp(state->jumpBuffer[state->current_depth]);
  if(ok) {
    state->current_depth++;
  } else {
    //We've had an exception update the stack.
    state->current_depth--;
  }
  return ok;
}

void throw_exception(struct exception_state * state) {
  longjump(state->current_depth-1,1);
}

void catch_point(struct exception_state * state) {
    state->current_depth--;
}

void end_try_point(struct exception_state * state) {
    state->current_depth--;
}

__thread struct exception_state g_exception_state; 

void Example() { 
  if (try_point(&g_exception_state)) {
    catch_point(&g_exception_state);
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
    end_try_point(&g_exception_state);
  }
}

void Test() {
  // Rough equivalent of `throw`
  throw_exception(g_exception_state);
}

Ancora una volta una versione più realistica di questo includerebbe un modo per memorizzare le informazioni sugli errori in exception_state, una migliore gestione di MAX_EXCEPTION_DEPTH(magari usando realloc per aumentare il buffer, o qualcosa del genere).

DISCLAIMER: Il codice sopra è stato scritto senza alcun test di sorta. È puramente così da avere un'idea di come strutturare le cose. Sistemi diversi e compilatori diversi dovranno implementare l'archiviazione locale del thread in modo diverso. Il codice probabilmente contiene sia errori di compilazione che errori logici, quindi mentre sei libero di usarlo come preferisci, TESTalo prima di usarlo;)


4

Una rapida ricerca su Google produce soluzioni kludgey come questa che usano setjmp / longjmp come altri hanno menzionato. Niente di così semplice ed elegante come try / catch in C ++ / Java. Sono piuttosto parziale nel gestire l'eccezione di Ada da solo.

Controlla tutto con le istruzioni if ​​:)


4

Questo può essere fatto con setjmp/longjmpin C. P99 ha un set di strumenti abbastanza comodo per questo che è anche coerente con il nuovo modello di thread di C11.


2

Questo è un altro modo per gestire gli errori in C che è più performante rispetto all'utilizzo di setjmp / longjmp. Sfortunatamente, non funzionerà con MSVC ma se usare solo GCC / Clang è un'opzione, potresti prenderla in considerazione. In particolare, utilizza l'estensione "etichetta come valore", che consente di prendere l'indirizzo di un'etichetta, memorizzarlo in un valore e saltarci sopra incondizionatamente. Lo presenterò usando un esempio:

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    /* Declare an error handler variable. This will hold the address
       to jump to if an error occurs to cleanup pending resources.
       Initialize it to the err label which simply returns an
       error value (NULL in this example). The && operator resolves to
       the address of the label err */
    void *eh = &&err;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    if (!engine)
        goto *eh; /* this is essentially your "throw" */

    /* Now make sure that if we throw from this point on, the memory
       gets deallocated. As a convention you could name the label "undo_"
       followed by the operation to rollback. */
    eh = &&undo_malloc;

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    if (!engine->window)
        goto *eh;   /* The neat trick about using approach is that you don't
                       need to remember what "undo" label to go to in code.
                       Simply go to *eh. */

    eh = &&undo_window_open;

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}

Se lo desideri, puoi effettuare il refactoring del codice comune nelle definizioni, implementando efficacemente il tuo sistema di gestione degli errori.

/* Put at the beginning of a function that may fail. */
#define declthrows void *_eh = &&err

/* Cleans up resources and returns error result. */
#define throw goto *_eh

/* Sets a new undo checkpoint. */
#define undo(label) _eh = &&undo_##label

/* Throws if [condition] evaluates to false. */
#define check(condition) if (!(condition)) throw

/* Throws if [condition] evaluates to false. Then sets a new undo checkpoint. */
#define checkpoint(label, condition) { check(condition); undo(label); }

Quindi l'esempio diventa

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    declthrows;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    checkpoint(malloc, engine);

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    checkpoint(window_open, engine->window);

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}

2

Attenzione: quanto segue non è molto carino ma fa il suo lavoro.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    unsigned int  id;
    char         *name;
    char         *msg;
} error;

#define _printerr(e, s, ...) fprintf(stderr, "\033[1m\033[37m" "%s:%d: " "\033[1m\033[31m" e ":" "\033[1m\033[37m" " ‘%s_error’ " "\033[0m" s "\n", __FILE__, __LINE__, (*__err)->name, ##__VA_ARGS__)
#define printerr(s, ...) _printerr("error", s, ##__VA_ARGS__)
#define printuncaughterr() _printerr("uncaught error", "%s", (*__err)->msg)

#define _errordef(n, _id) \
error* new_##n##_error_msg(char* msg) { \
    error* self = malloc(sizeof(error)); \
    self->id = _id; \
    self->name = #n; \
    self->msg = msg; \
    return self; \
} \
error* new_##n##_error() { return new_##n##_error_msg(""); }

#define errordef(n) _errordef(n, __COUNTER__ +1)

#define try(try_block, err, err_name, catch_block) { \
    error * err_name = NULL; \
    error ** __err = & err_name; \
    void __try_fn() try_block \
    __try_fn(); \
    void __catch_fn() { \
        if (err_name == NULL) return; \
        unsigned int __##err_name##_id = new_##err##_error()->id; \
        if (__##err_name##_id != 0 && __##err_name##_id != err_name->id) \
            printuncaughterr(); \
        else if (__##err_name##_id != 0 || __##err_name##_id != err_name->id) \
            catch_block \
    } \
    __catch_fn(); \
}

#define throw(e) { *__err = e; return; }

_errordef(any, 0)

Utilizzo:

errordef(my_err1)
errordef(my_err2)

try ({
    printf("Helloo\n");
    throw(new_my_err1_error_msg("hiiiii!"));
    printf("This will not be printed!\n");
}, /*catch*/ any, e, {
    printf("My lovely error: %s %s\n", e->name, e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err2_error_msg("my msg!"));
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printerr("%s", e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err1_error());
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printf("Catch %s if you can!\n", e->name);
})

Produzione:

Helloo
My lovely error: my_err1 hiiiii!

Helloo
/home/naheel/Desktop/aa.c:28: error: my_err2_error my msg!

Helloo
/home/naheel/Desktop/aa.c:38: uncaught error: my_err1_error 

Tieni presente che questo utilizza funzioni nidificate e __COUNTER__. Sarai al sicuro se utilizzi gcc.


1

Redis usa goto per simulare try / catch, IMHO è molto pulito ed elegante:

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success. */
int rdbSave(char *filename) {
    char tmpfile[256];
    FILE *fp;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
            strerror(errno));
        return REDIS_ERR;
    }

    rioInitWithFile(&rdb,fp);
    if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = REDIS_OK;
    return REDIS_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    return REDIS_ERR;
}

Il codice è rotto. errnodeve essere utilizzato solo subito dopo la chiamata di sistema non riuscita e non tre chiamate successive.
Ceving

Questo codice duplica la logica di gestione degli errori in più punti e potrebbe eseguire operazioni errate come chiamare più volte fclose (fp). Sarebbe molto meglio usare più etichette e codificare ciò che deve ancora essere recuperato usando quelle etichette (piuttosto che solo una per tutti gli errori) e poi saltare nel punto corretto di gestione degli errori a seconda di dove si verifica l'errore nel codice.
jschultz410

1

In C, puoi "simulare" eccezioni insieme alla "richiesta automatica di oggetti" tramite l'uso manuale di if + goto per la gestione esplicita degli errori.

Scrivo spesso codice C come il seguente (ridotto per evidenziare la gestione degli errori):

#include <assert.h>

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    if ( ( ret = foo_init( f ) ) )
        goto FAIL;

    if ( ( ret = goo_init( g ) ) )
        goto FAIL_F;

    if ( ( ret = poo_init( p ) ) )
        goto FAIL_G;

    if ( ( ret = loo_init( l ) ) )
        goto FAIL_P;

    assert( 0 == ret );
    goto END;

    /* error handling and return */

    /* Note that we finalize in opposite order of initialization because we are unwinding a *STACK* of initialized objects */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

Questo è ANSI C completamente standard, separa la gestione degli errori dal codice della linea principale, consente lo svolgimento (manuale) dello stack di oggetti inizializzati in modo molto simile a C ++, ed è completamente ovvio cosa sta succedendo qui. Poiché si verifica esplicitamente la presenza di errori in ogni punto, è più semplice inserire una registrazione specifica o una gestione degli errori in ogni punto in cui può verificarsi un errore.

Se non ti dispiace un po 'di magia macro, puoi renderlo più conciso mentre fai altre cose come la registrazione degli errori con le tracce dello stack. Per esempio:

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define TRY( X, LABEL ) do { if ( ( X ) ) { fprintf( stderr, "%s:%d: Statement '" #X "' failed! %d, %s\n", __FILE__, __LINE__, ret, strerror( ret ) ); goto LABEL; } while ( 0 )

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    TRY( ret = foo_init( f ), FAIL );
    TRY( ret = goo_init( g ), FAIL_F );
    TRY( ret = poo_init( p ), FAIL_G );
    TRY( ret = loo_init( l ), FAIL_P );

    assert( 0 == ret );
    goto END;

    /* error handling and return */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

Ovviamente, questo non è così elegante come le eccezioni C ++ + i distruttori. Ad esempio, annidare più stack di gestione degli errori all'interno di una funzione in questo modo non è molto pulito. Invece, probabilmente vorrai suddividerle in funzioni secondarie autonome che gestiscono in modo simile gli errori, inizializza + finalizza esplicitamente in questo modo.

Questo funziona anche solo all'interno di una singola funzione e non continuerà a saltare sullo stack a meno che i chiamanti di livello superiore non implementino una logica di gestione degli errori esplicita simile, mentre un'eccezione C ++ continuerà a saltare sullo stack finché non trova un gestore appropriato. Né ti consente di lanciare un tipo arbitrario, ma solo un codice di errore.

La codifica sistematica in questo modo (cioè con un unico punto di ingresso e di uscita) rende anche molto facile inserire la logica pre e post ("finalmente") che verrà eseguita a prescindere da cosa. Devi solo inserire la tua logica "finalmente" dopo l'etichetta END.


1
Molto bella. Tendo a fare qualcosa di simile. goto è ottimo per questo scenario. L'unica differenza è che non vedo la necessità di quell'ultimo "goto END", inserisco solo un ritorno di successo a quel punto, un ritorno fallito dopo il resto.
Neil Roy

1
Grazie @NeilRoy Il motivo del goto END è che mi piace che la maggior parte delle mie funzioni abbia un unico punto di ingresso e un unico punto di uscita. In questo modo, se voglio aggiungere un po 'di logica "finalmente" a qualsiasi funzione che posso sempre facilmente senza doversi preoccupare, ci sono altri ritorni nascosti in agguato da qualche parte. :)
jschultz410


-1

Forse non è una lingua principale (sfortunatamente), ma in APL c'è l'operazione ⎕EA (sta per Execute Alternate).

Utilizzo: "Y" ⎕EA "X" dove X e Y sono frammenti di codice forniti come stringhe o nomi di funzioni.

Se X si imbatte in un errore, al suo posto verrà eseguito Y (di solito gestione degli errori).


2
Ciao mappo, benvenuto in StackOverflow. Sebbene interessante, la domanda riguardava specificamente il fare questo in C. Quindi questo non risponde
luser droog
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.