Tratamento de exceções na linguagem C [Artigo] - página 5
[ Hits: 63.415 ]
Informação sobre os tipos em tempo de execução
Em C++, pode-se disparar como exceção qualquer tipo, e essa flexibilidade é desejável em muitos sistemas. Para permitir que o tratamento de exceções desenvolvido nesse texto trabalhe de um modo mais similar ao presente na linguagem C++ é necessário permitir que valores arbitrários sejam agregados às exceções disparadas.
É comum encontrarmos implementações de tratamento de exceções em C que utilizem pilhas como variáveis globais para fazer o tratamento de exceções.
Esse modelo, deveria, idealmente, permitir que utilizemos outros tipos além de inteiros para associarmos com as exceções disparadas e permitir que exceções não capturadas fossem tratadas por funções específicas, assim como ocorre em C++. Porém, para garantir que só há uma variável global e que todos a usem, deve-se utilizar a palavra reservada extern da linguagem C para declará-la sem definí-la e definí-la em um um único local em todo o código.
Isso requer um esforço extra para o programador, mas pode ser melhorado com o uso de bibliotecas estáticas ou dinâmicas, que é um esforço com o qual grande parte dos programadores de C já está acostumado. Contudo, uma grande limitação de tal modelo persiste, que é o a inadequação de seu uso em programação paralela, além de tornar a implementação demasiadamente complexa.
Na implementação descrita nesse texto, foi utilizada a passagem de ponteiros através de argumentos para permitir que funções, ao disparar exceções, retornem valores arbitrários associados com a exceção.
int divide(int a, int b, jmp_buf catch, void **exception)
// throw(const char *)
*exception = "Divisão por 0 impossível no conjunto dos números inteiros";
longjmp(catch, 1);
char *exception = NULL;
if (!setjmp(try)) {
c = divide(a, b, try, &exception);
// catch(char *)
printf("%s\n", exception);
// catch(...);
printf("Exceção desconhecida, impossível de tratar\n");
Utilizar simples ponteiros vazios, no entanto, nos impede de identificar quem disparou a exceção, o que torna inviável seu uso em problemas mais complexos, que é justamente onde o tratamento de exceções é mais adequado. Para contornar esse problema, programadores provavelmente utilizariam extensões, que, como consequência, quebraria a interoperabilidade do protocolo, novamente tornando seu uso inviável.
Para efetivamente resolver o problema, precisamos identificar os tipos em tempo de execução, assim como ocorre em C++. Há muitas abordagens para identificar tipos em tempo de execução. Uma abordagem que garante desempenho, é associar um valor inteiro a um tipo, porém novamente teria-se o problema de evitar colisões. Uma abordagem que parece mais atrativa, é usar uma string que contenha a assinatura do tipo para identificar o mesmo. Utilizando essa abordagem, sacrificamos um pouco de desempenho e ganhamos um recurso mais flexível para o trabalho.
struct _gvalue
int divide(int a, int b, jmp_buf catch, struct _gvalue *exception)
exception->name = "const char *";
exception->value = "Divisão por 0 impossível no conjunto dos números inteiros";
struct _gvalue exception = {NULL, NULL};
} else if (!strcmp(exception.name, "const char *"))
Uma das limitações desse modelo é que a busca por um bloco que trate exceções que sejam compatíveis com a disparada ocorre verificando a assinatura do tipo, e não o tipo em si. Dessa forma, conversões entre tipos não irão funcionar implicitamente, o que não é tão ruim, já que a proposta da linguagem é ser uma linguagem de médio nível.
Também, typedefs de tipos e assinaturas que usem convenções de nomes diferentes ("char *"/"char*", ...) serão reconhecidos como tipos novos, não sinônimos para tipos existentes, quebrando a consistência da linguagem. A solução que proponho é evitar o uso de typedefs ao disparar exceções e colocar um comentário nas funções que disparem exceções listando as assinaturas dos tipos que podem ser disparados com tais exceções.