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.

-------

 

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).