En C, vous pouvez "simuler" des exceptions avec une "récupération d'objets" automatique grâce à l'utilisation manuelle de if + goto pour une 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 ANSI C complètement standard, sépare la gestion des erreurs de votre code principal, permet le déroulement (manuel) de la pile d'objets initialisés un peu comme le fait C ++, et ce qui se passe ici est tout à fait évident. Étant donné que 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 cela ne vous dérange pas un peu de magie des macros, vous pouvez rendre cela plus concis tout en faisant d'autres choses comme la journalisation des 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 '" #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;
}
Bien sûr, ce n'est pas aussi élégant que les exceptions C ++ + destructeurs. Par exemple, imbriquer plusieurs piles de gestion des erreurs dans une fonction de cette manière n'est pas très propre. Au lieu de cela, vous voudrez probablement les diviser en sous-fonctions autonomes qui gèrent de la même manière les erreurs, initialiser + finaliser explicitement comme ceci.
Cela ne fonctionne également que dans une seule fonction et ne continuera pas à sauter dans la pile à moins que les appelants de niveau supérieur n'implémentent une logique de gestion des erreurs explicite similaire, alors qu'une exception C ++ continuera à sauter dans la pile jusqu'à ce qu'elle trouve un gestionnaire approprié. Il ne vous permet pas non plus de lancer un type arbitraire, mais uniquement un code d'erreur.
Le codage systématique de cette façon (c'est-à-dire avec une seule entrée et un seul point de sortie) rend également très facile l'insertion d'une logique pré et post ("enfin") qui s'exécutera quoi qu'il arrive. Vous venez de mettre votre logique «enfin» après l'étiquette END.