Je pensais aujourd'hui aux blocs try/catch existant dans d'autres langues. J'ai cherché sur Google pendant un certain temps, mais sans résultat. D'après ce que je sais, les try/catch n'existent pas en C. Cependant, existe-t-il un moyen de les "simuler" ?
Bien sûr, il existe des assertions et d'autres astuces, mais rien de comparable à try/catch, qui permettent également d'attraper l'exception soulevée. Je vous remercie.
Réponses
Trop de publicités?C'est une autre façon de gérer les erreurs en C qui est plus performante que l'utilisation de setjmp/longjmp. Malheureusement, elle ne fonctionnera pas avec MSVC mais si l'utilisation de GCC/Clang est une option, alors vous pouvez l'envisager. Plus précisément, elle utilise l'extension "label as value", qui vous permet de prendre l'adresse d'un label, de la stocker dans une valeur et de sauter à celle-ci sans condition. Je vais la présenter à l'aide d'un exemple :
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;
}
Si vous le souhaitez, vous pouvez remanier le code commun dans les définitions, en mettant effectivement en œuvre votre propre système de gestion des erreurs.
/* 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); }
L'exemple devient alors
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;
}
Une recherche rapide sur Google permet de trouver des solutions bancales telles que ce qui utilisent setjmp/longjmp comme d'autres l'ont mentionné. Rien d'aussi simple et élégant que les try/catch de C++/Java. Je suis moi-même plutôt favorable à la gestion des exceptions d'Ada.
Vérifiez tout avec des instructions if :)
Cela peut être fait avec setjmp/longjmp
en C. P99 dispose d'un ensemble d'outils assez confortable pour cela, qui est également compatible avec le nouveau modèle de fil de C11.
En C, vous pouvez "émuler" les exceptions ainsi que la "récupération d'objet" automatique par l'utilisation manuelle de if + goto pour la gestion explicite des erreurs.
J'écris souvent du code C comme le suivant (résumé pour mettre en évidence la gestion des erreurs) :
#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;
}
Il s'agit d'un C ANSI tout à fait standard, qui sépare la gestion des erreurs de votre code principal, permet le déroulement (manuel) de la pile des objets initialisés comme le fait le C++, et ce qui se passe ici est tout à fait évident. Comme vous testez explicitement l'échec à chaque point, il est plus facile d'insérer une journalisation spécifique ou une gestion des erreurs à chaque endroit où une erreur peut se produire.
Si un peu de magie des macros ne vous dérange pas, vous pouvez rendre cela plus concis tout en faisant d'autres choses, comme consigner les erreurs avec des traces de pile. Par exemple :
#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;
}
Bien sûr, ce n'est pas aussi élégant que les exceptions + les destructeurs du C++. Par exemple, l'imbrication de plusieurs piles de gestion des erreurs dans une fonction de cette façon n'est pas très propre. Au lieu de cela, vous voudrez probablement les séparer en sous-fonctions autonomes qui gèrent les erreurs, initialisent et finalisent explicitement comme ceci.
Cela ne fonctionne également qu'au sein d'une seule fonction et ne continuera pas à sauter sur la pile à moins que les appelants de niveau supérieur n'implémentent une logique de gestion d'erreur explicite similaire, alors qu'une exception C++ continuera à sauter sur la pile jusqu'à ce qu'elle trouve un gestionnaire approprié. Elle ne vous permet pas non plus de lancer un type arbitraire, mais seulement un code d'erreur.
En codant systématiquement de cette manière (c'est-à-dire avec un seul point d'entrée et un seul point de sortie), il est également très facile d'insérer une logique pré et post ("finally") qui s'exécutera quoi qu'il arrive. Il suffit de placer votre logique "finale" après l'étiquette END.