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.