Em C, você pode "simular" exceções junto com a "recuperação de objeto" automática por meio do uso manual de if + goto para tratamento explícito de erros.
Costumo escrever código C como o seguinte (resumido para destacar o tratamento de erros):
#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;
}
Isso é ANSI C completamente padrão, separa o tratamento de erros do seu código principal, permite o desenrolamento (manual) da pilha de objetos inicializados de maneira muito semelhante à do C ++ e é completamente óbvio o que está acontecendo aqui. Como você está testando explicitamente a falha em cada ponto, é mais fácil inserir logs específicos ou tratamento de erros em cada lugar em que um erro pode ocorrer.
Se você não se importa com um pouco de magia de macro, pode tornar isso mais conciso enquanto faz outras coisas, como registrar erros com rastreamentos de pilha. Por exemplo:
#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;
}
Claro, isso não é tão elegante quanto exceções C ++ + destruidores. Por exemplo, aninhar várias pilhas de tratamento de erros em uma função dessa maneira não é muito limpo. Em vez disso, você provavelmente gostaria de dividi-las em subfunções independentes que lidam de forma semelhante com erros, inicializar + finalizar explicitamente assim.
Isso também funciona apenas dentro de uma única função e não continuará pulando na pilha a menos que chamadores de nível superior implementem uma lógica de tratamento de erros explícita semelhante, enquanto uma exceção C ++ continuará pulando na pilha até encontrar um manipulador apropriado. Nem permite que você lance um tipo arbitrário, mas apenas um código de erro.
A codificação sistemática dessa forma (ou seja, com uma única entrada e um único ponto de saída) também torna muito fácil inserir a lógica pré e pós ("finalmente") que será executada independentemente do que aconteça. Você acabou de colocar sua lógica "finalmente" após o rótulo END.