Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação
Algoritmos e Estruturas de Dados I
Tratamento de exceção
A linguagem C++ dá
suporte ao tratamento de exceções feito basicamente através de uma metáfora
onde existe um lançamento de exceção (comando throw)
e o tratamento ou “apanhamento” do lançamento (comando try/catch).
A idéia
do tratamento de exceção é poder tornar o código mais claro com relação a o que
é considerado “normal” e o que é considerado “exceção” (ou anormal). No lado do
lançamento temos então o seguinte comportamento típico:
class ErroTipo1{};
class
ErroTipo2{};
void f(){
// código que implementa
o comportamento
if(erro1()){
ErroTipo1 t1;
throw
t1;
}
if(erro2()){
ErroTipo2 t2;
throw
t2;
}
}
Ou seja, toda função que
encontra algum problema de um certo tipo lança um elemento que seja
representativo do problema. Se uma função não faz nenhum lançamento então, de
forma convencional, ela não encontrou nenhum problema.
No lado do tratamento de
exceções temos o seguinte comportamento típico:
void g(){
try{
f();
}
catch(ErroTipo1 p1){/*
trata erro tipo 1*/}
catch(ErroTipo2
p2){/*trata erro tipo 2 */}
}
Ou seja, uma função ao
invocar um outra função para implementar um comportamento deve prover no “bloco
try” a especificação de comportamento sem problemas e
deve ainda prover um “bloco catch” para cada problema que possa ser lançado.
A semântica do comando throw pode ser resumida da seguinte forma: quando o fluxo
de controle chega ao comando “throw obj” é examinado se o comando está em um bloco try com uma clausula catch apropriada ou compatível ao obj (veja abaixo “Clausula catch compatível”). caso
positivo o fluxo de controle desvia para a clausula catch (veja abaixo “Desvio para clausula catch”). se este throw não está em um bloco try
com clausula catch apropriada então é salvo o endereço de retorno do registro
de ativação corrente e este último é destruido (após
ações adequadas, p.ex. chamadas de destruidores) e, no contexto dado pelo
endereço de retorno, novamente é feito o exame com relação a existir um bloco try com clausula catch compatível com obj.
A execução do comando throw segue portanto pela
cadeia dinâmica dos registros de ativação até encontrar uma clausula catch
compatível. Se todos os registros de ativação forem destruídos, inclusive da
função main() então o ambiente de tempo de execução
emite uma mensagem de erro dizendo que houve o lançamento de uma exceção que
não foi tratada.
A semântica do comando try/catch pode ser descrita de forma simplificada da
seguinte forma: quando o fluxo de controle chega até o comando try/catch serão executados os comandos existentes dentro do
bloco try. Não havendo lançamento de exceções então
as clausulas catch não são executadas e o fluxo de controle segue para o
comando seguinte ao comando try/catch sem que nenhuma
clausula catch seja executada. Se houver lançamento de exceção então o fluxo de
controle poderá ser desviado do ponto de lançamento da exceção para uma das
clausulas catch compativeis, mais particularmente
para a mais próxima em termos da cadeia dinâmica de execução e dentre estas
a primeira compatível na seqüência do texto Após
a execução de uma clausula catch o fluxo de controle segue para o comando
seguinte ao comando try/catch.
Clausula “catch”compativel
As regras de
compatibilidade são baseadas na noção de tipo. Além da compatibilidade
obvia do tipo <throw p;> é compatível com
<catch(T e)> onde p é do tipo T, temos também a possibilidade <throw p> ser compatível com <catch(T e) onde p, mesmo
não sendo do tipo T, é de um tipo S descendente de T.
Desvio para clausula
catch
O desvio de controle para
uma clausula catch envolve uma preparação do parâmetro da clausula (se
houver!), portanto se existir o parâmetro ele sofrera a atribuição do elemento
que foi lançado. podem ser lançados valores, ponteiro, objetos, dentre outros.
A garantia da compatibilidade de atribuição vem da compatibilidade do tipo
usado na clausula catch e o tipo do elemento lançado.
Para poder apreciar a
procura “destrutiva” de um tratador de exceção através da cadeia dinâmica dos
registros de ativação tente entender o programa abaixo:
#include <iostream>
using namespace std;
class A{public: char *id; bool silencio;
A(char *p):silencio(false){id=p;}
void podeIr(){silencio=true;}
~A(){if(!silencio)cout<<id<<": me destruiram!\n";}
};
void f(){throw 1; printf("f()\n");}
void d(){A x("D"); f(); x.podeIr();}
void c(){A x("C"); d(); x.podeIr();}
void b(){A x("B"); c(); x.podeIr();}
void a(){A x("A"); b(); x.podeIr();}
int main (void) {
try{a();}catch(int i){printf("peguei int\n");}
system("PAUSE");
return EXIT_SUCCESS;
}
Para a execução deste
programa primeiramente é criado o registro de ativação de main(). Na seqüência de comandos de main
dentro de um bloco try é invocada a função a, que
chama b, que chama c, que chama d, que chama f, ou seja são criados 5 registros
de ativação além do registro de ativação de main.
antes de cada chamada, cada uma das funções define um objeto da seguinte forma
a() cria um A(“A”), b() cria um A(“B”), c() cria um A(“C”) e d() cria um
A(“D”). Se a função f() não fizesse nenhum lançamento de exceção haveria
oportunidade do fluxo de execução atingir as funções “podeIr()”
em cada uma das funções d(), c(), b() e a() e os objetos seriam destruidos silenciosamente. Como f() lança uma exceção a excução deste comando throw sai
destruindo os registros de ativação (e chamando os destruidores dos objetos!),
e, ao longo desta execução, os objetos estão no estado onde silencio =false e
os destruidores escrevem mensagem sobre a destruição. Transforme em comentário
o lançamento de exceção em f() e poderá ser observado que os objetos serão destruidos de forma silenciosa. Os objetos são destruidos na ordem inversa da criação e os destruidores
são chamados de forma consistente com a destruição.
A linguagem C++ permite
ainda que a definição das funções declare se poderá haver a execução de
lançamento de exceções:
(i) uma função que nada
declara pode lançar o que quiser:
void f(){ /*... throw new QualquerCoisa; throw valor; */}
(ii)
uma função com uma “especificação de tipos para lançamentos de exceções” só
poderá executar lançamentos de instâncias do tipo especificado:
void f() throw(int){ /* throw 3; */}
(iii)
uma função com uma lista de exceções vazia não permite a execução de
lançamentos (o compilador permite a presença de lançamentos!)
void f()throw() {/* nenhum
lançamento poderá ser executado! */}
Quando uma função descumpre
sua promessa e lança algo que ela prometeu não lançar o Código de Tempo de
Execução – CTE nao fará o lançamento e irá invocar
uma função denominada unexpected().
O programador caso queira “tratar” tal tipo de ocorrência deverá ter chamado
antes a função set_unexpected()
passando como parâmetro uma sua função. Caso a função set_unexpected
tenha sido chamada o ponteiro unexpected_handler
ficara apontando para a função passada como parâmetro, caso contrário unexpected_handler ficará com o valor default que aponta
para terminate(). A função terminate()
normalmente invoca a função default do ponteiro terminate_handler,
denominada abort().
O programador pode ter chamado antes a função set_terminate() passando como parâmetro uma função a ser chamada no lugar
da abort().
O programa abaixo ilustra
o uso de set_unexpected:
#include <cstdlib>
#include <iostream>
using
namespace std;
void
f()throw(){ //prometo não lancar
nada
throw
1; //a quebra de promessa é compilavel!
}
void
trata(){
cout<<"veio
pra ca!\n";
system("PAUSE");
}
int
main(int argc, char *argv[]){
set_unexpected(trata);
f();
system("PAUSE");
return
EXIT_SUCCESS;
}
O tratamento mais
cuidadoso exige maior disciplina e salvamento adequado de informação.
--------
A linguagem C++ provê uma
forma de apanhar qualquer lançamento, trata-se de usar reticências (3 pontos)
na cláusula catch. A cláusula catch(...) [apanhador curinga] irá apanhar
qualquer lançamento. A ordem dos apanhadores é importante e o apanhador curinga
deve aparecer em último lugar na lista de apanhadores em um bloco de
tentativa/experimentação/prova (try).
-----
O livro de 1997 do Stroustrup discute a seguinte hierarquia de exceções:

O programa em C++ abaixo
ilustra o lançamento de out_of_range pela classe string
#include <iostream>
#include <stdexcept>
using
namespace std;
int
main (void) {
string
s("abc");
try
{
s.at(1)='X';
s.at(12)='A';
}
catch (out_of_range& p) {
cerr
<< "Out of Range error:
" << p.what() <<"
"<<s<< endl;
}
system("pause");
return
0;
}
Um aspecto que pode parecer estranho é a atribuição a uma chamada de função: (s.at(1)=’X’;). O operador de atribuição (=) é sobrecarregado e
isto corresponde a uma invocação parecida com (existem mais fatores):
s.at(1).operator=('X'); // at()
devolve uma referência
O aprendizado de C++
envolve, dentre outros aspectos, saber quais classes/métodos lançam exceções e
qual a melhor forma de tratá-las.
-------
Os projetistas das linguagem C++ as vezes mostram “tolerância” com um estilo mais moderno ou um
estilo mais antigo/convencional de lidar com exceções. O operador “new” quando não consegue atender uma solicitação de alocação
de memória lança uma instância da classe bad_alloc, como pode ser visto no programa
abaixo:
#include <iostream>
using namespace std;
class X{public:double
dd[10];};
int main() {
try{
X *p=new X[3000000000];
}catch(bad_alloc
p){cout<<"bad alloc!" <<endl;}
return 0;
}
O programa acima
tem um estilo “mais moderno”... mas a linguagem C++
permite a seguinte sintaxe para o operador “new”:
new (nothrow)
<tipo>[ <expressão>]
Neste caso não há
lançamento de exceção e devemos verificar se foi ou não atendida a alocação verificando se o resultado é um ponteiro nulo:
X
*p=new (nothrow) X[3000000000];
if(p==NULL)
cout<<”não alocou”<<endl;
No trecho acima ficaria
mais “moderno” (!) o uso de um novo literal da linguagem C++ denominado nullptr if(p==nullptr)
Vale a pena observar que
a linguagem C tem um mecanismo que permite a “simulação” de transferências de
controle similares às permitidas pelo par de comandos (i)throw e (2) try catch. A
linguagem C define uma função denominada setjmp() que
estabelece um contexto associado a um registro de ativação. Esta função setjmp() teria “proximidade” com a operacionalização do
comando try/catch. A linguagem C define ainda uma
função denominada longjmp() que ao ser invocada não
retorna para o comando que a segue imediatamente (!). Esta função faz com que o
fluxo de controle se dirija para para o ponto onde
foi invocada a função setjmp(). Todos os registros de
ativação que tenham sido construidos desde a última
chamada de setjmp() são destruidos(!)
e o fluxo de controle é tomado a partir do comando onde houve a invocação de setjmp(). De um certo ponto de vista é como se uma chamada
de setjmp tivesse “dois” retornos. No primeiro
retorno(que só envolve a própria setjmp) a chamada
devolve o valor 0 no segundo retorno (que envolve a função longjmp)
a função setjmp retorna um valor, diferente de zero,
conforme especificado pela função longjmp(). Esta
função longjmp() teria “proximidade” com a
operacionalização do comando throw.
O exemplo abaixo foi extraido da Wikipedia:
#include <stdio.h>
#include <setjmp.h>
static jmp_buf buf;
void second(void){
printf("second\n"); // prints
longjmp(buf,1); // jumps back to where setjmp was called - making setjmp now return 1
}
void first(void){
second();
printf("first\n"); // does not print
}
int main(){
if (!setjmp(buf)){
first(); // (first time), setjmp returns 0 (observe “not”)
}else{ // when longjmp jumps back, setjmp returns 1
printf("main\n"); // prints
}
return 0;
}
Como o nome da função é algo do tipo “ajustaSalto” pode gerar confusão o fato de que quando ela é chamada exatamente para “ajustar o salto” ela devolve 0 (falso).