Probeer catch-statements in C

Ik dacht vandaag aan de try/catch-blokken die in andere talen bestaan. Heb hier even op gegoogled maar zonder resultaat. Voor zover ik weet, bestaat er niet zoiets als try/catch in C. Is er echter een manier om ze te “simuleren”?
Natuurlijk, er zijn assert- en andere trucs, maar er gaat niets boven try/catch, die ook de verhoogde uitzondering opvangen. Bedankt


Antwoord 1, autoriteit 100%

C zelf ondersteunt geen uitzonderingen, maar je kunt ze tot op zekere hoogte simuleren met setjmpen longjmp-aanroepen.

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);
}

Deze website heeft een mooie tutorial over het simuleren van uitzonderingen met setjmpen longjmp


Antwoord 2, autoriteit 24%

U gebruikt gotoin C voor een soortgelijke fout omgaan met situaties.
Dat is het dichtste equivalent van uitzonderingen die u in C kunt krijgen.


Antwoord 3, autoriteit 16%

Ok, ik kon het niet laten om hierop te reageren. Laat ik eerst zeggen dat ik het geen goed idee vind om dit in C te simuleren, aangezien het echt een vreemd concept is voor C.

We kunnen gebruikende preprocessor en lokale stackvariabelen misbruiken om een beperkte versie van C++ try/throw/catch te gebruiken.

Versie 1 (lokale scope-worpen)

#include <stdbool.h>
#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

Versie 1 is alleen een lokale worp (kan het bereik van de functie niet verlaten). Het is wel afhankelijk van het vermogen van C99 om variabelen in code te declareren (het zou in C89 moeten werken als de try het eerste is in de functie).

Deze functie maakt alleen een lokale var zodat deze weet of er een fout is opgetreden en gebruikt een goto om naar het catch-blok te springen.

Bijvoorbeeld:

#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;
}

Dit komt neer op zoiets als:

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

Versie 2 (scope springen)

#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);

Versie 2 is een stuk ingewikkelder, maar werkt in principe op dezelfde manier. Het gebruikt een
verspringen uit de huidige functie naar het try-blok. Het try-blok dan
gebruikt een if/else om het codeblok over te slaan naar het catch-blok dat de local . controleert
variabele om te zien of het moet vangen.

Het voorbeeld is weer uitgebreid:

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;
}

Dit gebruikt een globale aanwijzer zodat de longjmp() weet welke poging de laatste keer is uitgevoerd.
We gebruikenen misbruiken de stapel, zodat onderliggende functies ook een try/catch-blokkering kunnen hebben.

Het gebruik van deze code heeft een aantal keerzijden (maar is een leuke mentale oefening):

  • Het zal geen toegewezen geheugen vrijmaken omdat er geen deconstructors worden aangeroepen.
  • Je kunt niet meer dan 1 try/catch in een scope hebben (geen nesting)
  • Je kunt eigenlijk geen uitzonderingen of andere gegevens genereren zoals in C++
  • Helemaal niet veilig voor threads
  • Je stelt andere programmeurs in om te falen omdat ze de hack waarschijnlijk niet zullen opmerken en ze proberen te gebruiken als C++ try/catch-blokken.

Antwoord 4, autoriteit 9%

In C99,kunt u setjmp/longjmpvoor niet-lokale controlestroom.

Binnen een enkel bereik gebruikt het generieke, gestructureerde coderingspatroon voor C in de aanwezigheid van meerdere resourcetoewijzingen en meerdere exits goto, zoals in dit voorbeeld. Dit is vergelijkbaar met hoe C++ destructor-aanroepen van automatische objecten onder de motorkap implementeert, en als je je hier ijverig aan houdt, zou het je een zekere mate van zuiverheid moeten bieden, zelfs in complexe functies.


Antwoord 5, autoriteit 6%

Hoewel sommige van de andere antwoorden de eenvoudige gevallen hebben behandeld met behulp van setjmpen longjmp, zijn er in een echte toepassing twee problemen die er echt toe doen.

  1. Nesten van try/catch-blokken. Als u een enkele globale variabele voor uw jmp_bufgebruikt, werken deze niet.
  2. Inrijgen. Een enkele globale variabele voor jou jmp_bufzal in deze situatie allerlei soorten pijn veroorzaken.

De oplossing hiervoor is om een thread-local stack van jmp_bufte onderhouden die gaandeweg wordt bijgewerkt. (Ik denk dat dit is wat lua intern gebruikt).

Dus in plaats van dit (van het geweldige antwoord van 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);
}

Je zou zoiets gebruiken als:

#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);
}

Ook een meer realistische versie hiervan zou een manier zijn om foutinformatie op te slaan in de exception_state, een betere afhandeling van MAX_EXCEPTION_DEPTH(misschien met behulp van realloc om de buffer te laten groeien, of zoiets).

DISCLAIMER: De bovenstaande code is geschreven zonder enige test. Het is puur zodat je een idee krijgt van hoe je dingen kunt structureren. Verschillende systemen en verschillende compilers zullen de lokale opslag van de thread anders moeten implementeren. De code bevat waarschijnlijk zowel compileerfouten als logische fouten – dus terwijl je vrij bent om het te gebruiken zoals je wilt, TEST het voordat je het gebruikt;)


Antwoord 6, autoriteit 4%

Een snelle Google-zoekopdracht levert kludgey oplossingen op zoals dezedie setjmp/longjmp gebruiken als anderen hebben genoemd. Niets zo rechttoe rechtaan en elegant als C++/Java’s try/catch. Ik ben nogal een voorstander van Ada’s uitzonderingen die ik zelf afhandel.

Controleer alles met if-statements 🙂


Antwoord 7, autoriteit 4%

Dit kan met setjmp/longjmpin C. P99heeft hiervoor een redelijk comfortabele toolset die ook consistent is met het nieuwe threadmodel van C11.


Antwoord 8, autoriteit 3%

Dit is een andere manier om foutafhandeling in C uit te voeren die beter presteert dan het gebruik van setjmp/longjmp. Helaas werkt het niet met MSVC, maar als het gebruik van alleen GCC/Clang een optie is, zou je het kunnen overwegen. Het gebruikt met name de extensie “label as value”, waarmee u het adres van een label kunt nemen, het in een waarde kunt opslaan en er onvoorwaardelijk naartoe kunt springen. Ik zal het aan de hand van een voorbeeld presenteren:

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;
}

Als u dat wenst, kunt u gemeenschappelijke code in definities refactoren, zodat u uw eigen foutafhandelingssysteem effectief implementeert.

/* 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); }

Dan wordt het voorbeeld

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;
}

Antwoord 9, autoriteit 2%

Waarschuwing: het volgende is niet erg aardig, maar het doet zijn werk.

#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)

Gebruik:

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);
})

Uitvoer:

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’ 

Houd er rekening mee dat dit geneste functies en __COUNTER__gebruikt. U bent aan de veilige kant als u gcc gebruikt.


Antwoord 10, autoriteit 2%

In C kunt u uitzonderingen “emuleren” samen met automatische “objectterugwinning” door handmatig gebruik van if + goto voor expliciete foutafhandeling.

Ik schrijf vaak C-code zoals de volgende (samengekookt om foutafhandeling te benadrukken):

#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;        
}

Dit is volledig standaard ANSI C, scheidt de foutafhandeling van uw hoofdregelcode, maakt (handmatige) stapelafwikkeling van geïnitialiseerde objecten mogelijk, net zoals C++ dat doet, en het is volkomen duidelijk wat hier gebeurt. Omdat u op elk punt expliciet op fouten test, wordt het gemakkelijker om specifieke logboekregistratie of foutafhandeling in te voegen op elke plaats waar een fout kan optreden.

Als je een beetje macromagie niet erg vindt, kun je dit beknopter maken terwijl je andere dingen doet, zoals het loggen van fouten met stacktraces. Bijvoorbeeld:

#include <assert.h>
#include <stdio.h>
#include <string.h>
#define TRY( X, LABEL ) do { if ( ( X ) ) { fprintf( stderr, "%s:%d: Statement '%s' failed! %d, %s\n", __FILE__, __LINE__, #X, 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;        
}

Natuurlijk is dit niet zo elegant als C++-uitzonderingen + destructors. Het op deze manier nesten van meerdere stapels voor foutafhandeling binnen één functie is bijvoorbeeld niet erg schoon. In plaats daarvan zou je die waarschijnlijk willen opsplitsen in op zichzelf staande subfuncties die op dezelfde manier omgaan met fouten, initialiseren + expliciet afronden op deze manier.

Dit werkt ook alleen binnen een enkele functie en blijft niet op de stapel springen, tenzij bellers van een hoger niveau soortgelijke expliciete logica voor foutafhandeling implementeren, terwijl een C++-uitzondering gewoon op de stapel blijft springen totdat het een geschikte handler vindt. Het staat je ook niet toe om een willekeurig type te gooien, maar in plaats daarvan alleen een foutcode.

Door op deze manier systematisch te coderen (d.w.z. met een enkele ingang en een enkele uitgang) is het ook heel gemakkelijk om pre en post (“eindelijk”) logica in te voegen die hoe dan ook zal worden uitgevoerd. Je zet gewoon je “eindelijk” logica achter het END label.


Antwoord 11

Redis gebruikt goto om try/catch te simuleren, IMHO is het erg schoon en elegant:

/* 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;
}

Antwoord 12

Als u C gebruikt met Win32, kunt u gebruik maken van de Structured Exception Handling (SEH)om try/catch te simuleren.

Als je C gebruikt op platforms die setjmp()en longjmp()niet ondersteunen, bekijk dan deze Afhandeling van uitzonderingenvan de pjsip-bibliotheek, het biedt wel zijn eigen implementatie


Antwoord 13

Misschien geen grote taal (helaas), maar in APL is er de ⎕EA-bewerking (staat voor Execute Alternate).

Gebruik:
‘Y’ EA ‘X’
waarbij X en Y codefragmenten zijn die worden geleverd als tekenreeksen of functienamen.

Als X een fout tegenkomt, wordt in plaats daarvan Y (meestal foutafhandeling) uitgevoerd.

Other episodes