Guião da 13ª Aula Teórica

!!Este guião transformou-se num capítulo...  Transformar num resumo e escrever novo guião...

Sumário

  1. Tratamento básico de erros ocorridos durante a execução de programas não críticos:
    1. Origens dos erros: lógicos, utilizador, recursos externos.
    2. Tratamento de erros: instruções usuais (condicionais, de selecção, de iteração), asserções e excepções.
    3. Origens humanas imediatas (programador e utilizador) vs. recursos externos (e.g., formato de ficheiros, existência de ficheiros, limitações de memória, etc.).
    4. Papeis do humano: programador produtor, programador consumidor e utilizador final.
    5. Protecção contra erros lógicos (do programador):
    6. Protecção contra erros do utilizador:
    7. Protecção contra erros com origem em recursos externos:

Guião

!!

Distinguir excepções de erros desde o início

Usar hierarquias separadas existentes no C++ para erros (runtime) e excepções

Sugerir nova instrução de asserção com excepções?  Ver propostas Alexandrescu e Boost.

Explicar: Excepções C++ usados para lidar com erros e com casos excepcionais.  Casos excepcionais podem ser falhas (e não erros) em recursos externos.  O utilizador engana-se (não erra).

asserções abortivas: suicídio brutal (e.g., comboio), sem elegância nem consideração pelos outros.

asserções lançando excepções: permitem harakiri, com elegância e consideração pelos outros (tentar causar o mínimo de danos), além de antes do golpe final ainda ter tempo para contactar Deus pai confessando os seus pecado (enviar para a empresa produtora informação acerca do estado do programa).

!!

Nesta aula vamos falar de tratamento básico de erros.  Note-se que este é um assunto complicado em geral.  Vamos assumir que estamos a desenvolver uma aplicação "normal".  I.e., vamos admitir que uma falha do programa é muito inconveniente mas não é dramática (nem fatal).  É o caso, por exemplo, de um processador de texto.  Se fosse um programa de controlo de tráfego aéreo, de cálculo de trajectórias de mísseis, de controlo do arrefecimento de uma central nuclear, teríamos de usar estratégias muito diferentes e nada triviais.

Comecemos por identificar as possíveis fontes de erros num programa:

Discutir com eles!  Concluir em:

  1. Utilizador humano do programa.
  2. Erros do programador (quer como programador consumidor quer como programador produtor).
  3. Recursos externos inadequados (errados, indisponíveis ou insuficientes).
Há um princípio básico na programação: o utilizador errará.
Há outro princípio básico na programação: o utilizador falhará na pior ocasião (a única não prevista)

Como deve o programa lidar com erros do utilizador?

Discutir

É garantido que os utilizadores humanos do programa se vão enganar.  É inevitável.  Com estes erros devemos lidar explicitamente no programa verificando e lidando com os erros onde quer que eles possam ocorrer.

Há outro princípio na programação: o programador errará.
Há ainda outro princípio na programação: alguns os erros do programador resistirão a todos os teste e revelar-se-ão apenas depois de o programa ter sido distribuido ao seu utilizador final...

Como se deve lidar com estes erros?

Discutir.  Não é preciso concluir nada.  Deixar para depois.

E os recursos externos?  Estes são recursos que estão fora do controlo de programador e do programa propriamente ditos.  Exemplos:

Por outro lado, não é razoável pedir ao computador para corrigir estes erros, tal como se faria com um humano!

Como se deve lidar com estes erros?

Discutir.  Não é preciso concluir nada.  Deixar para depois.

Note-se que os erros têm muitas vezes origem humana.  O utilizador do programa é tipicamente um humano.  O programador, quer como programador consumidor quer como programador produtor, é também um humano.  Muitas vezes é o mesmo humano que desempenha alternada ou simultaneamente papéis diferentes!  Um programador fabrica código usando código já escrito, compila e testa o programa, assumindo neste processo os três papéis.

O que temos de descobrir é como se deve lidar em geral com os vários tipos de erros.  Como ferramentas temos as instruções de selecção, condicionais ou de iteração da própria linguagem, as asserções e as excepções.  Quando é adequado usar cada uma destas ferramentas?

Vamos ver alguns programas de exemplo e estudar os possíveis erros que podem ocorrer no seu contexto.

Por exemplo, suponham um programa simples para cálculo duma raiz quadrada pelo método de Newton:

Explicar vagamente o método e dizer para eles deduzirem a expressão de progressão enquanto passo o programa no quadro.  Deixar claro que não é a forma mais eficiente de calcular a raiz quadrada...

#include <iostream>
#include <limits>

using namespace std;

/** Devolve uma aproximação da raiz quadrada de valor.
   
@pre 0 <= valor.
    @post |raizDe × raizDe - valor| <= e × v, onde e (epsilon) é a diferença entre o
          menor double maior que 1 e 1, representando portanto de alguma
          forma o limite mínimo de erro alcançável.
          e = numeric_limits<double>::epsilon(). */
double raizDe(double const valor)
{
    double raiz_anterior = 0.0;

    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;

        raiz = 0.5 * (raiz_anterior + valor / raiz_anterior);
    }

    return raiz;
}

int main()
{
    cout << "Introduza um valor: ";
    double valor_lido;
    cin >> valor_lido;
    cout << "raiz de " << valor_lido << " é "
         << raizDe(valor_lido) << endl;

}

Que acontece se o valor dado pelo utilizador for negativo?  E se o utilizador pressionar 'a' em vez de um número?

Se o valor for negativo a função entra em ciclo infinito!

De quem é o problema?  Do programador, obviamente!  Ele deve garantir que as pré-condições da função se verificam.  Mas qual programador?  Quem programou a função?  Ou quem programou a função main()?  Bom, os dois, mas de formas totalmente diferentes.

Quem programa a função (o programador produtor) não pode assumir muito acerca de futuras utilizações.  Responsabiliza-se pelo valor devolvido pela função apenas se as pré-condições se verificarem.  É esse o contrato que estabelece com o programador consumidor da função.  Mas pode ser simpático e garantir que, enquanto o programa estiver em teste, a passagem de argumentos que não cumpram a PC levem à terminação do programa com uma mensagem apropriada.  Para isso usam-se asserções.

Note-se que o programador da função está a prepará-la para detectar erros de outros programadores, os programadores consumidores (que podem ser a mesma pessoa)!  A função não sabe nada acerca do humano que usa o programa!  Assim:

#include <cassert>

...

double raizDe(double const valor)
{
    assert(0.0 <= valor);

    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;

        raiz = 0.5 * (raiz_anterior + valor / raiz_anterior);
    }

    return raiz;
}

...

Desse modo, o código fornecedor da função contém uma protecção contra erros no código cliente.

É importante perceber que a colocação de uma asserção para verificação das pré-condições da função serve para proteger o programador consumidor dessa função dos seus próprios erros.  Por outro lado, é possível também o programador produtor proteger-se dos seus próprios erros colocando uma asserção para verificação das condições objectivo da função.

Neste caso a CO é razoavelmente complicada, pelo que não se entra em pormenores:

#include <cstdlib>

...

double raizDe(double const valor)
{
    assert(0.0 <= valor);

    double raiz_anterior = 0.0;
    double raiz = valor;

    while(raiz != raiz_anterior) {
        raiz_anterior = raiz;

        raiz = 0.5 * (raiz_anterior + valor / raiz_anterior);
    }

    assert(abs(raiz * raiz - valor) <= 
          
numeric_limits<double>::epsilon() * valor);

    return raiz;
}

...

Desta forma se o programador produtor se enganar ao escrever a função, a asserção falhará e assinalará que a aproximação não é suficientemente boa.

As asserções têm uma vantagem adicional: se falharem o programa termina não apenas com uma mensagem de erros mais ou menos inteligível mas também gerando um ficheiro de "core".  Este ficheiro contém uma imagem completa do processo no momento em que a asserção falhou.  Esta imagem pode ser usada para determinar ou pelo menos para facilitar a determinação das causas para a falha.  Para isso basta executar o depurador e dizer-lhe para carregar a imagem do processo.  Se o programa se chamasse "raiz", por exemplo, bastava fazer:

gdb raiz
core core

A partir deste instante é possível verificar o valor de todas as variáveis no momento da falha.  Para isso pode ser útil usar o comando up, que sobe um nível na hierarquia de chamadas de funções e procedimentos, até que a função ou procedimento mostrado seja um dos que estão em desenvolvimento.

Agora, se o utilizador do programa introduzir um valor negativo o programa aborta com uma mensagem antipática.  Isso é útil durante o teste do programa.  Mas muito indesejável para o seu utilizador!  De quem é a culpa?  Do programador produtor de main() e utilizador de raizDe()!  Ele deveria verificar se o valor dado pelo utilizador é aceitável!  A asserção tem como grande utilidade mostrar claramente ao programador de main() que há algo de errado com o seu código.  Ele vai verificar este erro durante os testes do programa.  Presumivelmente antes de entregar o programa ao utilizador final.

Note-se que há aqui três papeis diferentes: o programador produtor de raizDe(), o programador consumidor de raizDe() e o utilizador do programa!  Durante o desenvolvimento do programa estes podem ser papeis tomados alternadamente pela mesma pessoa.

A solução passa pois por reconhecer que o utilizador final se pode enganar e escrever o programa à prova desses erros!

...

void lêValorNãoNegativoPara(double& valor_lido)
{
    while(true) {
        cout << "Introduza um valor: ";
        cin >> valor_lido;

        if(0.0 <= valor_lido)
            return;

        cout << "O valor tem de ser não negativo!" << endl;
    }
}

int main()
{
    double valor_lido;
    lêValorNãoNegativoPara(valor_lido);
    cout << "raiz de " << valor_lido << " é " 
         << raizDe(valor_lido) << endl;

}

Fazer uma nota acerca do estranho ciclo!  Explicar que está entre um while e um do while.  Não é uma heresia...

Feito isto e testado o programa, ficou garantido que a função raiz será chamada com um valor não negativo.  Nesse caso a verificação das asserções já não são necessárias!  Logo, pode-se retirá-la do código.  Hmmm.....  Será boa ideia?  Não!  O programa ainda pode precisar de acrescentos, melhorias, correcções, e portanto de ser testado de novo!  Se isso acontecer dá muito jeito que a asserção lá continue!  Como resolver o problema então?  É ineficiente estar lá quando o programa está testado mas é conveniente que lá continue porque podemos precisar de testar o programa de novo!

Solução: desligar as asserções.  Como?  Compilando com a opção -DNDEBUG (no debug).

Para acelerar o teste de uma aplicação é comum distribuir versões não finais, as chamadas versões alfa ou beta, a utilizadores seleccionados.  Estas versões devem ser distribuidas sem desligar as asserções.  É que se pode pedir aos utilizadores para, em caso de erro, enviarem uma mensagem de correio electrónico com:
1.  Mensagem de erro gerada (identifica a asserção falhada).
2.  Ficheiro imagem: permite inspecção do estado do programa.
3.  Descrição breve dos passos que geraram o erro: facilita perceber como se atingiu o estado de erro.

Problema resolvido...  De certeza?  E se o utilizador escrever uma letra?  Aí a leitura falha!  E pior, o canal fica num estado de erro em que todas as tentativas de leituras posteriores falham também...  Fica em ciclo infinito!

Explicar.

Solução:

...

void ignoraLinha()
{
    cin.clear();

    char caractere;
    do
        cin.get(caractere);
    while(not cin.fail() and caractere != '\n');

}

void lêValorNãoNegativoPara(double& valor_lido)
{
    while(true) {
        cout << "Introduza um valor: ";
        cin >> valor_lido;

        if(not cin.fail() and 0.0 <= valor_lido)
            return;

       if(cin.fail()) {
             ignoraLinha();
             cout << "Tem de ser um número real!" << endl;
        } else // valor_lido < 0.0

            cout << "O valor tem de ser não negativo!" << endl;
    }
}

...

Mas, e a terceira fonte de erros?  Erros devido a recursos externos ao programa?  Exemplos?  Falta de memória, falta de espaço em disco, ficheiro inexistente, ficheiro com formato errado...  Há aqui algo que me cheira a trabalho final...

!!!!!!!!!Este exemplo baseia-se no trabalho final de 1999/2000.  Pode precisar de ser refeito em anos posteriores...

!!!!A parte final, sobre construtor por cópia, clonagem, etc., deveria ser feita antes!  Implementar desenha como stub (tradução?) que escreve no ecrã o que deveria desenhar.

!!!!!!!!!!!Ao passar este exemplo para folhas teóricas é conveniente basear-me na hierarquia de formas desenvolvida nos capítulos anteriores.  Talvez essa hierarquia de formas deva passar a usar o Slang++....

Suponhamos que queríamos escrever uma classe Polígono usando o Slang++ e que essa classe deveria estar integrada numa hierarquia de conceitos encimada pela classe abstracta Forma:

!!!!!!!!!Nas folhas teóricas pôr Slang::.

!!!!!cumpreInvariante!!!!!

class Forma /* simplificada... */ {
  public:
    // Com origem em origem:
    Forma(Posicao const& origem);

    // Tem de haver sempre no topo das hierarquias polimórficas:
    virtual ~Forma() {}

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const = 0;

    // Todas as formas podem ser desenhadas no ecrã:
    virtual desenha(bool seleccionada = false) = 0;

    // Devolve origem da forma:
    Posicao const& origem() const;

    // Move a forma estabelecendo uma nova origem:
    void movePara(Posicao const& nova_origem);

    // Move a forma deslocando a sua origem:
    void moveDe(Posicao const& deslocamento);

  private:
    Posicao origem_;
};

class Polígono : public Forma {
  public:
    // Sem vértices com origem em origem:
    Poligono(Posicao const& origem);

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const;

    // O vertice v tem coordenadas absolutas...
    void novoVértice(Posicao const& v);

    virtual void desenha(bool seleccionado = false);
    ...

  private:
    list<Posicao> vertices;
};

A ideia é que um polígono é representado pela sequência dos seus vértices, admitindo-se que cada vértice se liga por uma aresta aos vértices adjacentes na sequência, sendo que o primeiro e o último vértice se consideram adjacentes.  Note-se que a posição dos vértices é relativa à origem do polígono, que é uma posição convencional.

Mas pretendíamos mais.  Deveria ser possível carregar e guardar polígonos de e em ficheiros...  Para isso acrescentaríamos:

  1. Um construtor para construir um polígono a partir de informação lida de um canal.
  2. Um procedimento carregaDe() para carregar informação de um canal, descartando a informação anteriormente guardada no polígono.
  3. Um procedimento guardaEm() para guardar a informação escrevendo num canal.
  4. E faríamos o mesmo a todas as classes da hierarquia...
Ou seja:

!!!!!carregaDe e guardaEm

class Forma {
  public:
    // Com origem em origem:
    Forma(Posicao const& origem);
    // Lida de canal:
    Forma(istream& entrada);

    // Tem de haver sempre no topo das hierarquias polimórficas:
    virtual ~Forma() = 0;

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const = 0;

    // Procedimentos para guardar e carregar:
    virtual void carrega(istream& entrada);
    virtual void guarda(ostream& saida) const;

    // Todas as formas podem ser desenhadas no ecrã:
    virtual desenha(bool seleccionada = false) = 0;

    // Devolve origem da forma:
    Posicao const& origem() const;

    // Move a forma estabelecendo uma nova origem:
    void movePara(Posicao const& nova_origem);

    // Move a forma deslocando a sua origem:
    void moveDe(Posicao const& deslocamento);

  private:
    Posicao origem_;
};

class Polígono : public Forma {
  public:
    // Sem vértices com origem em origem:
    Poligono(Posicao const& origem);

    // Lido de canal:
    Poligono(istream& entrada);

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const;

    // Procedimentos para guardar e carregar:
    virtual void carrega(istream& entrada);
    virtual void guarda(ostream& saida) const;

    virtual void desenha(bool seleccionado = false) const;

    // O vertice v tem coordenadas absolutas...
    void novoVértice(Posicao const& v);

  private:
    list<Posicao> vertices;
};

Discutir implementação.

inline Forma::Forma(Posicao const& origem)
    : origem_(origem) 
{

}

inline Forma::Forma(istream& entrada)
    : origem_(entrada) 
{

    // Que acontece se o construtor de Posicao falhar?
}

inline Forma::~Forma()
{
}

inline void Forma::carrega(istream& entrada) 
{

    origem_.carrega(entrada);
    // Que acontece se o procedimento Posicao::carrega() falhar?
}

inline void Forma::guarda(ostream& saida) 
{

    origem_.guarda(saida);
    // Que acontece se o procedimento Posicao::guarda() falhar?
}

inline Posicao const& Forma::origem() const 
{

    return origem_;
}

inline void Forma::movePara(Posicao const& nova_origem) 
{

    origem_ = nova_origem;
}

inline void Forma::moveDe(Posicao const& deslocamento) 
{

    origem += deslocamento;
}

inline Poligono::Poligono(Posicao const& origem)
    : Forma(origem) 
{

}

Poligono::Poligono(istream& entrada)
    : Forma(entrada)
    // Que acontece se o construtor de Forma falhar?
{
    int numero_de_vertices;
    if(!(entrada >> numero_de_vertices))
        /* Que fazer se a extracção do número de vértices falhar? */;
    while(numero_de_vertices-- != 0)
        vertices.push_back(Posicao(entrada));
    // Que acontece se o construtor de Posicao falhar?
}

inline string const Poligono::nomeDoTipo() const 
{

    return "Poligono";
}

Usar idioma do swap!

void Poligono::carrega(istream& entrada)
{
    Forma::carrega(entrada);
    // Que acontece se o procedimento Forma::carrega() falhar?

    vertices.clear();

    int numero_de_vertices;
    if(!(entrada >> numero_de_vertices))
        /* Que fazer se a extracção do número de vértices falhar? */;
    while(numero_de_vertices-- != 0)
        vertices.push_back(Posicao(entrada));
    // Que acontece se o construtor de Posicao falhar?
}

void Poligono::guarda(ostream& saida) const
{
    Forma::guarda(saida);
    // Que acontece se o procedimento Forma::guarda() falhar?

    if(!(saida << vertices.size()))
        /* Que fazer se a inserção do número de vértices falhar? */;
    for(list<Posicao>::const_iterator i = vertices.begin(); i != vertices.end(); ++i)
        i->guarda(saida));
    // Que acontece se o procedimento Posicao::guarda() falhar?
}

inline void Poligono::novoVertices(Posicao const& v)
{

    // O vertice v tem coordenadas absolutas, pelo que há que lhe retirar a origem do polígono:
    vertices.push_back(v - origem());
}

void Poligono::desenha(bool seleccionado) const
{
    ...
}

Para evitar repetições de código pode-se factorizar o código repetido num procedimento privado auxiliar da classe Poligono:

!!!!Não!  Com o idioma do swap é inútil!

!!!!!Não repetir tudo!

class Forma {
  public:
    // Com origem em origem:
    Forma(Posicao const& origem);
    // Lida de canal:
    Forma(istream& entrada);

    // Tem de haver sempre no topo das hierarquias polimórficas:
    virtual ~Forma() {}

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const = 0;

    // Procedimentos para guardar e carregar:
    virtual void carrega(istream& entrada);
    virtual void guarda(ostream& saida) const;

    // Todas as formas podem ser desenhadas no ecrã:
    virtual desenha(bool seleccionada = false) = 0;

    // Devolve origem da forma:
    Posicao const& origem() const;

    // Move a forma estabelecendo uma nova origem:
    void movePara(Posicao const& nova_origem);

    // Move a forma deslocando a sua origem:
    void moveDe(Posicao const& deslocamento);

  private:
    Posicao origem_;
};

class Polígono : public Forma {
  public:
    // Sem vértices com origem em origem:
    Poligono(Posicao const& origem);

    // Lido de canal:
    Poligono(istream& entrada);

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const;

    // Procedimentos para guardar e carregar:
    virtual void carrega(istream& entrada);
    virtual void guarda(ostream& saida) const;

    virtual void desenha(bool seleccionado = false) const;

    // O vertice v tem coordenadas absolutas...
    void novoVértice(Posicao const& v);

  private:
    list<Posicao> vertices;

    void carregaEspecifico(istream& entrada);
};

inline Forma::Forma(Posicao const& origem)
    : origem_(origem) {
}

inline Forma::Forma(istream& entrada)
    : origem_(entrada) {
    // Que acontece se o construtor de Posicao falhar?
}

inline void Forma::carrega(istream& entrada) {
    origem_.carrega(entrada);
    // Que acontece se o procedimento Posicao::carrega() falhar?
}

inline void Forma::guarda(ostream& guarda) {
    origem_.guarda(saida);
    // Que acontece se o procedimento Posicao::guarda() falhar?
}

inline Posicao const& Forma::origem() const {
    return origem_;
}

inline void Forma::movePara(Posicao const& nova_origem) {
    origem_ = nova_origem;
}

inline void Forma::moveDe(Posicao const& deslocamento) {
    origem += deslocamento;
}

inline Poligono::Poligono(Posicao const& origem)
    : Forma(origem) {
}

inline Poligono::Poligono(istream& entrada)
    : Forma(entrada) {
    // Que acontece se o construtor de Forma falhar?
    carregaEspecifico(entrada);
    // Que acontece se o procedimento carregaEspecifico() falhar?
}

inline string const Poligono::nomeDoTipo() const {
    return "Poligono";
}

void Poligono::carrega(istream& entrada)
{
    Forma::carrega(entrada);
    // Que acontece se o procedimento Forma::carrega() falhar?

    vertices.clear();

    carregaEspecifico(entrada);
}

void Poligono::guarda(ostream& saida) const
{
    Forma::guarda(saida);
    // Que acontece se o procedimento Forma::guarda() falhar?

    if(!(saida << vertices.size()))
        /* Que fazer se a inserção do número de vértices falhar? */;
    for(list<Posicao>::const_iterator i = vertices.begin(); i != vertices.end(); ++i)
        i->guarda(saida));
    // Que acontece se o procedimento Posicao::guarda() falhar?
}

void Poligono::carregaEspecifico(istream& entrada)
{
    int numero_de_vertices;
    if(!(entrada >> numero_de_vertices))
        /* Que fazer se a extracção do número de vértices falhar? */;
    while(numero_de_vertices-- != 0)
        vertices.push_back(Posicao(entrada));
    // Que acontece se o construtor de Posicao falhar?
}

inline void Poligono::novoVertices(Posicao const& v) {
    // O vertice v tem coordenadas absolutas, pelo que há que lhe retirar a origem do polígono:
    vertices.push_back(v - origem());
}

void Poligono::desenha(bool seleccionado) const
{
    ...
}

Problemas:  que fazer quando a leitura ou a escrita falham?

Em primeiro lugar há que reconhecer que as causas para uma falha são normalmente externas: os recursos usados para leitura e escrita são tipicamente ficheiros.  Estes recursos externos não estão totalmente sob o controlo do programador...

Por um lado é claro que se uma leitura de um canal falhar não se pode pedir de novo os dados, pelo simples facto de não se poder assumir que existe um ente inteligente do outro lado do canal: a maior parte das vezes o canal estará ligado a um ficheiro que, se não tiver o formato correcto, dificilmente se auto-corrigirá...

Por outro lado também não é aceitável que o programa simplesmente aborte quando alguma leitura falhar!  Se você fosse utilizador de um programa tão temperamental certamente rogaria pragas ao seu programador.

Logo, é necessário que as funções e os procedimentos assinalem os possíveis erros para que o código onde são invocados possa recuperar desses erros.  Uma solução clássica mas problemática é fazer os procedimentos devolver um valor booleano verdadeiro se tudo tiver corrido bem e falso no caso contrário...  Por exemplo:

inline bool Forma::carrega(istream& entrada) {
    return origem_.carrega(entrada);
    // Que acontece se o procedimento Posicao::carrega() falhar?  Devolve-se false.
}

bool Poligono::carrega(istream& entrada)
{
    if(!Forma::carrega(entrada))
        return false;
    // Que acontece se o procedimento Forma::carrega() falhar?  Devolve-se false.

    vertices.clear();

    return carregaEspecifico(entrada);
}

bool Poligono::carregaEspecifico(istream& entrada)
{
    int numero_de_vertices;
    if(!(entrada >> numero_de_vertices))
        return false;
    while(numero_de_vertices-- != 0)
        vertices.push_back(Posicao(entrada));
    // Opps...  Construtores não podem devolver valores...
    // Que acontece se o construtor de Posicao falhar?
    return true;
}

Esta solução tem dois graves problemas:
  1. Não é aplicável no caso dos construtores, pois não devolvem qualquer valor.  (Também se pode argumentar que devolvem o objecto construído, mas nesse caso é claro que não podem devolver um booleano...)
  2. Obriga o programador consumidor do código a verificar sistematicamente o valor devolvido pelos procedimentos.  Como isso é muito aborrecido, o programador tende a "distrair-se", e os erros ficam por tratar...
  3. Todas as funções e procedimentos tem de lidar com os erros explicitamente: ou recuperando desses erros ou limitando-se a assinar a sua ocorrência devolvendo um valor falso.  Toda a sequência de funções e procedimentos invocados desde o local onde ele pode ser corrigido (recuperação) até à sua origem têm de lidar desta forma com os erros.  Note-se que o local onde se pode corrigir um erro (recuperar) e o local onde se detecta o erro podem estar muito distantes...  Isto é mais uma vez muito maçador e portanto leva a esquecimentos fatais pelos programadores.
Discutir soluções!

A solução ideal é a utilização de excepções.   As vantagens são:

  1. Podem-se lançar excepções inclusive nos construtores.
  2. O programador consumidor não é obrigado a lidar com as excepções.  Se ninguém capturar uma excepção lançada o programa aborta gerando um ficheiro de imagem core, que pode ser analisado como indicado acima para verificar as razões que levaram ao lançamento da excepção.
  3. Só lida com as excepções quem sabe o que fazer com elas!  Assim, código que não sabe que fazer caso seja lançada uma excepção limita-se a não se preocupar: ou algum código mais exterior se encarregará de recuperar dessa excepção ou o programa abortará.  Só é obrigado a lidar com uma excepção o código que pretende recuperar do erro que a originou.
O Slang++ já utiliza excepções.  Os procedimentos para guardar informação em canais lançam a excepção ErroAoGuardar em caso de erro e os procedimentos para carregar lançam a excepção ErroAoCarregar.  Essas classes fazem parte de uma pequena hierarquia de excepções com o seguinte aspecto:
// Esta classe serve de base a uma pequena hierarquia de classes representando excepções.
class Erro {
  public:
    // Construtor da classe. mensagem é uma mensagem explicando a origem da excepção.
    Erro(std::string const& mensagem)
        : mensagem(mensagem) {
    }

    // Destrutor virtual para poder sofrer derivações...
    virtual ~Erro() {
    }

    /* Inspector da mensagem explicando a origem da excepção na forma de uma conversão implícita
       para std::string. */
    virtual operator std::string () const {
        return mensagem;
    }

  private:
    // A mensagem explicando a origem da excepção.
    std::string mensagem;
};

// Esta classe serve para representar excepções de carregamento de objectos a partir de canais.
class ErroAoCarregar : public Erro {
  public:
    // Construtor da classe. classe é o nome da classe que originou a excepção.
    ErroAoCarregar(std::string const& classe)
        : Erro(std::string("Erro ao carregar '") + classe + "'") {
    }
};

// Esta classe serve para representar excepções ao guardar objectos usando canais.
class ErroAoGuardar : public Erro {
  public:
    // Construtor da classe. classe é o nome da classe que originou a excepção.
    ErroAoGuardar(std::string const& classe)
        : Erro(std::string("Erro ao guardar '") + classe + "'") {
    }
};

Assim, deve-se acrescentar código para lançar excepções apropriadas em caso de erro:
class Forma {
  public:
    // Com origem em origem:
    Forma(Posicao const& origem);
    // Lida de canal:
    Forma(istream& entrada);

    // Tem de haver sempre no topo das hierarquias polimórficas:
    virtual ~Forma() {}

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const = 0;

    // Procedimentos para guardar e carregar:
    virtual void carrega(istream& entrada);
    virtual void guarda(ostream& saida) const;

    // Todas as formas podem ser desenhadas no ecrã:
    virtual desenha(bool seleccionada = false) = 0;

    // Devolve origem da forma:
    Posicao const& origem() const;

    // Move a forma estabelecendo uma nova origem:
    void movePara(Posicao const& nova_origem);

    // Move a forma deslocando a sua origem:
    void moveDe(Posicao const& deslocamento);

  private:
    Posicao origem_;
};

class Polígono : public Forma {
  public:
    // Sem vértices com origem em origem:
    Poligono(Posicao const& origem);

    // Lido de canal:
    Poligono(istream& entrada);

    // Todos os tipos concretos de formas têm um nome:
    virtual string const nomeDoTipo() const;

    // Procedimentos para guardar e carregar:
    virtual void carrega(istream& entrada);
    virtual void guarda(ostream& saida) const;

    virtual void desenha(bool seleccionado = false) const;

    // O vertice v tem coordenadas absolutas...
    void novoVértice(Posicao const& v);

  private:
    list<Posicao> vertices;

    void carregaEspecifico(istream& entrada);
};

Forma::Forma(Posicao const& origem)
    : origem_(origem) {
}

Forma::Forma(istream& entrada)
    : origem_(entrada) {
    // E se for lançada uma excepção no construtor de Posicao?  Deixa-se passar...
}

void Forma::carrega(istream& entrada) {
    origem_.carrega(entrada);
    // E se for lançada uma excepção no procedimento Posicao::carrega()?  Deixa-se passar...
}

void Forma::guarda(ostream& saida) {
    origem_.guarda(saida);
    // E se for lançada uma excepção no procedimento Posicao::guarda()?  Deixa-se passar...
}

Posicao const& Forma::origem() const {
    return origem_;
}

void Forma::movePara(Posicao const& nova_origem) {
    origem_ = nova_origem;
}

void Forma::moveDe(Posicao const& deslocamento) {
    origem += deslocamento;
}

inline Poligono::Poligono(Posicao const& origem)
    : Forma(origem) {
}

Poligono::Poligono(istream& entrada)
    : Forma(entrada)
    // E se for lançada uma excepção no construtor de Forma?  Deixa-se passar...
{
    carregaEspecifico(entrada);
    // E se for lançada uma excepção no procedimento carregaEspecifico()?  Deixa-se passar...
}

inline string const Poligono::nomeDoTipo() const {
    return "Poligono";
}

void Poligono::carrega(istream& entrada)
{
    Forma::carrega(entrada);
    // E se for lançada uma excepção no procedimento Forma::carrega()?  Deixa-se passar...

    vertices.clear();

    carregaEspecifico(entrada);
    // E se for lançada uma excepção no procedimento carregaEspecifico()?  Deixa-se passar...
}

void Poligono::guarda(ostream& saida) const
{
    Forma::guarda(saida);
    // E se for lançada uma excepção no procedimento Forma::guarda()?  Deixa-se passar...

    // Em caso de erro na inserção lança-se explicitamente uma excepção!  Note-se que as
    // inserções (operador <<) normalmente não lançam excepções, assinalando erro ao colocar o
    // canal em estado de erro, que tem de ser verificado explicitamente.
    if(!(saida << vertices.size()))
        throw ErroAoGuardar("Poligono");

    for(list<Posicao>::const_iterator i = vertices.begin();
        i != vertices.end(); ++i)
        i->guarda(saida));
        // E se for lançada uma excepção no procedimento Posicao::guarda()?  Deixa-se passar...
}

void Poligono::carregaEspecifico(istream& entrada)
{
    // Em caso de erro na extracção lança-se explicitamente uma excepção!  Note-se que as
    // extracções (operador >>) normalmente não lançam excepções, assinalando erro ao colocar o
    // canal em estado de erro, que tem de ser verificado explicitamente.
    int numero_de_vertices;
    if(!(entrada >> numero_de_vertices))
        throw ErroAoCarregar("Poligono");

    while(numero_de_vertices-- != 0)
        vertices.push_back(Posicao(entrada));
        // E se for lançada uma excepção no construtor de Posicao?  Deixa-se passar...
}

inline void Poligono::novoVertices(Posicao const& v) {
    // O vertice v tem coordenadas absolutas, pelo que há que lhe retirar a origem do polígono:
    vertices.push_back(v - origem());
}

void Poligono::desenha(bool seleccionado) const
{
    ...
}

Note-se:
  1. Se o construtor da Forma falhar será lançada uma excepção!  O construtor de Forma limita-se a construir uma posição.  Essa construção é que lança a excepção.  O programador da classe Forma não tem de se preocupar com o assunto!
  2. Idem se houverem erros nos procedimentos Forma::carrega() e Forma::guarda().
  3. Os procedimentos lançam excepções explicitamente caso ocorra algum erro que não resulte no lançamento de uma uma excepção.  É o caso dos procedimentos Poligono::guarda() e Poligono::carregaEspecifico().
  4. Os procedimentos não recuperam de erros.  Se detectarem erros, lançam excepções.  Se os erros forem em funções ou procedimentos por eles invocados, limitam-se a ignorar as excepções por eles lançados.
Quem é responsável por recuperar dos erros?  Apenas que entender dever fazê-lo.  Suponha-se uma classe Figura que representa o conceito de Figura como uma agregação de Formas:
class Figura {
  public:
    Figura(istream& entrada);

    void carrega(istream& entrada);
    void guarda(ostream& saida);

    ...

    void desenha() const;

  private:
    list<Forma*> formas;
};

inline Figura::Figura(istream& entrada) {
    carrega(entrada);
}

void Figura::carrega(istream& entrada)
{
    // Em caso de erro na extracção lança-se explicitamente uma excepção!  Note-se que as
    // extracções (operador >>) normalmente não lançam excepções, assinalando erro ao colocar o
    // canal em estado de erro, que tem de ser verificado explicitamente.
    int numero_de_formas;
    if(!(entrada >> numero_de_formas))
        throw ErroAoCarregar("Figura");

    while(numero_de_formas-- != 0) {
            string tipo_de_forma;
            if(!(entrada >> tipo_de_forma))
                throw ErroAoCarregar("Figura");
            if(tipo_de_forma == "Poligono")
                formas.push_back(new Poligono(entrada));
            else if(tipo_de_forma == ...)
               ...
            else
                // Tipo de figura desconhecido.  Talvez merecesse uma excepção mais específica...
                throw ErroAoCarregar("Figura");
        }
        // E se for lançada uma excepção no construtor de Poligono?  Deixa-se passar...
}

void Figura::guarda(ostream& saida) const
{
    // Em caso de erro na inserção lança-se explicitamente uma excepção!  Note-se que as
    // inserções (operador <<) normalmente não lançam excepções, assinalando erro ao colocar o
    // canal em estado de erro, que tem de ser verificado explicitamente.
    if(!(saida << formas.size()))
        throw ErroAoGuardar("Figura");

    for(list<Forma*>::const_iterator i = formas.begin(); i != formas.end();
        ++i) {
        if(!(saida << (*i)->nomeDoTipo()))
            throw ErroAoGuardar("Figura");
        (*i)->guarda(saida));
        // E se for lançada uma excepção no procedimento Forma::guarda()?  Deixa-se passar...
    }
}

Repare-se como se resolveu o problema de carregar e guardar os objectos usando o nome do seu tipo (é o problema da persistência dos objectos).

Suponha-se ainda uma classe Editor, com a responsabilidade de criar e editar figuras:

class Editor {
  public:
    ...

    void carrega();
    void guarda() const;

    ...
  private:
    bool figura_alterada;
    Figura* figura;
};

void Editor::carrega()
{
    string nome = CaixaDeTexto("Ficheiro a carregar:").executa();
    if(ifstream entrada(nome.c_str())) {
        try {
            Figura* nova_figura = new Figura(entrada);
            if(figura_alterada &&
               MenuSimNao("Quer guardar a figura corrente?").executa())
                guarda();
            delete figura;
            figura = nova_figura;
        } catch(Erro& e) {
            Aviso(e).executa();
        } catch(bad_alloc) {
            Aviso("Memória insuficiente!").executa();
        } catch(...) {
            Aviso("Erro desconhecido ao carregar figura!").executa();
        }
    } else
        Aviso("Não consegui abrir!").executa();
}


É claro que a classe Figura não quer lidar com possíveis erros de carregamento, por exemplo.  Que poderia ela fazer?  Por outro lado a classe Editor tem obrigação de fazer alguma coisa.  Essa classe é responsável, presume-se, pela interação com o utilizador do programa e por isso tem a responsabilidade de capturar excepções durante o carregamento de figuras, se elas ocorrerem, e delicadamente avisar o utilizador do programa e dar-lhe a chance de prosseguir o seu trabalho.

Note-se a estratégia seguida pelo editor: a figura anterior só é descartada quando a nova foi carregada com sucesso.  Assim, em caso de falha, o utilizador é avisado e continua a editar a figura anterior.

É importante perceber-se que as excepções podem, e muitas vezes devem, ser capturadas bastante longe do local onde são lançadas.  Repare-se no que sucede se ocorrer um erro na leitura da origem de um polígono da nova figura.  A excepção é lançada pelo construtor da classe Posicao (do Slang++), invocado no construtor de Forma, invocado no construtor de Poligono, invocado no construtor de Figura, invocado no procedimento carrega() da classe Editor, que finalmente a captura!

Note-se também que a herança permite a definição de hierarquias de excepções que podem ser usadas depois para capturas mais específicas ou mais genéricas.  Neste caso fez-se a captura de excepções da classe genérica Erro, que se presume representarem qualquer possível tipo de erro do Slang++ (note-se a declaração da excepção capturada como referência de modo a poder fazer uso do polimorfismo).  Como podem ser lançadas excepções de outros tipos, nomeadamente bad_alloc se não houver memória ao criar alguma variável dinâmica, fazem-se capturas em sequência, chegando-se ao extremo de capturar qualquer excepção que não seja capturada pelos catch anteriores.

!!!!!!!!!!!!!!!Daqui para baixo são conceitos avançados!

O que se disse até agora é só parte da verdade...  É que se uma excepção for lançada a meio de uma operação complexa (e.g., um carregamento) pode haver objectos que fiquem num estado inválido!  Isso é muito perigoso!  Esta questão tem de ser resolvida pelos programadores produtores.  Estes não se podem limitar a lançar excepções em caso de erro e ignorar as excepções com que não queiram lidar...  Podem fazê-lo mas apenas se garantirem que todos os objectos ficam num estado aceitável.  A isto chama-se "segurança face a excepções" [stroustrup].  Segundo Stroustrup [??] há três níveis de segurança face a excepções que podem ser garantidos pelos programadores produtores de uma dada função ou procedimento membro ou não membro:

  1. Nível básico: todos os objectos ficam num estado válido (ou seja, verificando a CII da sua classe).
  2. Nível forte: ou há total sucesso ou é lançada uma excepção e a função ou procedimento não tem quaisquer efeitos, deixando os objectos exactamente no mesmo estado que tinham antes do seu início.
  3. Sem lançamento: a função ou procedimento não lançará nenhuma excepção (directa ou indirectamente), tendo sempre sucesso.
Em nenhum dos casos são aceitáveis fugas de memória, como é óbvio.

É claro que se deve lutar sempre por dar as garantias mais fortes!  Mas isso não deve ser feito à custa de uma complexidade exagerada dos programas ou de grandes perdas de eficiência.

Para ajudar neste processo, a linguagem C++ faz algumas garantias.  A mais importante delas diz que ao se abandonar uma função ou procedimento devido a uma excepção todos os objectos locais construído são automaticamente destruídos.  Claro está que esta garantia significa que se for lançada uma excepção num construtor, o objecto em construção não será deixado semi-construído: todos os atributos (variáveis e constantes membros) entretanto construídos, assim como as classes base, serão automaticamente destruídos.  Estas garantias são simpáticas, mas não resolvem todos os problemas.  Variáveis dinâmicas não são destruídas automaticamente, por exemplo.  Em geral, qualquer recurso externo reservado numa função ou procedimento no qual seja lançada uma excepção (explicita ou implicitamente) devem ser libertados sob risco de haver fugas...

A conclusão a que se chega, portanto, é que a interface de uma função ou procedimento não consiste apenas no seu cabeçalho (que indicam como se usa) e das pré-condição e condição objecto (que indicam o que se compromete a fazer e em que circunstâncias): inclui também informação acerca das garantias de segurança face a excepções (que indicam o que sucede aos objectos envolvidos em casos excepcionais).

Analisemos as garantias que são dadas no código apresentado:

Classe Forma

Se, num construtor da classe Forma, for lançada uma excepção na construção do atributo origem_, a construção não tem sucesso.  O bom comportamento da classe Forma face a erros durante a construção do atributo origem_ depende por isso do bom comportamento dos construtors da classe Posicao.

E durante o procedimento carrega() da classe Forma?  Como a classe Forma se limita a invocar o procedimento carrega() da classe Posicao, tudo depende do bom comportamento desse procedimento.

Acontece que quer os construtores da classe Posicao quer o seu procedimento carrega() oferecem o nível mais forte de segurança face ao lançamento de excepções (esta garantia faz parte da interface da classe).  Assim, também os construtores da classe Forma e o seu procedimento carrega() oferecem o o nível mais forte de segurança face a excepções.

E o procedimento guarda()?  Aqui o problema é diferente.  É que guardar não altera senão o canal onde se escreve: o objecto a guardar nunca é alterado.  Como não é possível voltar atrás no que se escreveu num canal de saída genérico, a ideia é que se um procedimento guarda() falhar, deve-se considerar como lixo tudo o que foi escrito no canal...  No fundo isto significa que, do ponto de vista do objecto forma, o procedimento oference o nível forte de segurança, mas do ponto de vista do canal não, limitando-se a oferecer o nível básico de segurança.

Calma...  E o canal de entrada no caso do construtor a partir de um canal e do procedimento carrega(), ambos da classe Forma?  Não se pode simplesmente "des-ler" aquilo que já foi lido (por acaso até pode, mas com limitações).  Por isso, também essas operações oferecem apenas o nível básico de segurança relativamente ao canal de entrada...

Cientes das nuances face aos canais de entrada e saída apontadas, daqui em diante, para simplificar a discussão, o estado destes canais deixará de ser tido em conta.

Classe Poligono

Este caso é mais interessante.  O construtor de um polígono vazio ao qual se passa apenas a origem oferece naturalmente a segurança de não lançar qualquer excepção.

Por outro lado o construtor a partir de um canal começa por invocar o construtor da classe Forma.  Como este oferece o nível forte de segurança, não há qualquer problema.  Em seguida é invocado implicitamente o construtor por omissão da lista de vértices.  Também este construtor oferece o nível forte de segurança face a excepções.  Finalmente, já no corpo do construtor, é invocado o procedimento carregaEspecífico().  Se durante a sua execução for lançada uma excepção toda a construção do Poligono falha, sendo invocados os destrutores da lista de vértices e da classe base.  Perfeito.

O procedimento carrega() da classe Poligono é mais complicado.  Começa por invocar o procedimento carrega() da classe Forma, que oferece o nível máximo de segurança face a excepções.  O problema é que depois invoca o procedimento carregaEspecifico(), que pode falhar...  Vamos admitir que o procedimento carregaEspecífico() se comporta "decentemente", i.e., ou consegue carregar aquilo que é específico de um polígono (a lista dos vértices), ou mantém a lista de vértices como estava.  Neste caso temos um problema complicado.  É que se o carregaEspecifico() falhar, só parte do polígono foi carregado, nomeadamente a sua origem, atra'ves do procedimento Forma::carrega(), ficando a lista de vértices como estava...  Para que seja oferecido o nível forte de segurança há que garantir que se consegue voltar atrás no carregamento.  A solução típica é:

void Poligono::carrega(istream& entrada)
{
    Poligono novo(entrada);
    *this = novo;
}
Ou seja, a solução passou por recorrer no carrega() ao construtor da classe.  Como o construtor oferece o nível forte de segurança, uma de duas coisas podem ocorrer:
  1. A construção tem sucesso.  Nesse caso o novo polígono é copiado para a instância implícita e depois destruído (implitamente).
  2. A construção falha.  Nesse caso não só o novo polígono não chega a ser construído como a execução não chega à atribuição devido ao lançamento de uma excepção, ficando o polígono intacto.
Claro está que esta solução tem uma desvantagem: é ineficiente.  O que acontece de facto ao longo da sua execução?
  1. É construído um novo polígono lido de um canal.
  2. O novo polígono é atribuído à instância implícita.  Ou seja:
    1. É esvaziada a lista de vértices da instância implícita.
    2. São copiados os vértices da nova lista para a lista da instãncia implícita.
  3. Finalmente é invocado o destrutor do novo polígono, levando à destruição da respectiva lista de vértices.
O problema está na feitura de uma cópia.  Como evitá-la?

Outro problema da solução acima é que a atribuição entre polígonos pode lançar excepções...  Porquê?  Porque implica fazer uma cópia dos vértices, e pode não haver memória para isso!

Os dois problemas podem-se resolver de uma forma simples.  As listas da biblioteca padrão do C++ disponibilizam uma forma rápida de troca de itens: o procedimento membro swap.  Pode-se então reescrever:

void Poligono::carrega(istream& entrada)
{
    Poligono novo(entrada);
    // Copia o que é genérico (nunca lança excepção):
    Forma::operator = (novo);
    // Troca o que é específico (nunca lança excepção):
    vertices.swap(novo.vertices);
}
Como a atribuição entre duas Forma não lança excepções e o mesmo se passa com o procedimento swap(), esse problema está resolvido.

Também fica resolvido o problema da cópia.  Agora as listas são trocadas (sem cópia), o que faz com que a lista dos vértices antigos vá parar ao novo polígono, sendo destruída quando esta variável for destruída, i.e., ao se atingir a chaveta final do procedimento.

Classe Figura

A discussão para as figuras é semelhante à discussão para os polígonos, pelo que não se trata aqui.

Assuntos por resolver

Uma questão que não se abordou aqui, mas deveria ter sido abordada, é a dos construtor e operador de atribuição por cópia das classes que recorrem a variáveis dinâmicas: Editor e Figura.

O caso da figura é particularmente interessante.  O construtor por cópia limita-se a criar uma nova figura contendo ponteiros para as mesmas formas da figura original.  Isso é ou não desejável?  Duas figuras são iguais se possuírem as mesmas formas ou se possuírem formas iguais?  O que nos interessa é semântica de valor ou de cópia?

Em qualquer dos casos põem-se problemas muito interessantes, que serão abordados mais tarde.  ????

!!!!!!!!!Semântica de referência: figuras não podem destruir as suas formas!  Que o faz, então?  Solução: ponteiros inteligentes com contadores de referências?  Ou contadores de referências dentro da classe?

!!!!!!!!!!!!Semântica de valor: como construir cópias de objectos heterogéneos?  Quando se tem um ponteiro polimórfico e se constrói uma nova variável dinâmica igual ao objecto apontado por esse ponteiro diz-se que se fez uma clonagem.  A solução passa por usar uma função membro virtual chamada clona(), que devolve um clone, e que é implementada apropriadamente por todas as classes da hierarquia:

class Base { // Abstracta
  public:
    Base(Base const&); // construtor por cópia explícito ou implícito.
    Base* clona() const = 0; // a clonagem é puramente virtual nas classes abstractas e virtual nas concretas.
};

classe Derivada : public Base { // Concreta
  public:
    Derivada(Derivada const&); // idem.
    virtual Derivada* clona() const {
        return new Derivada(*this);
    }
};

A estas funções de clonagem também se chama construtores virtuais, por razões óbvias.

Curiosamente no padrão Compósito (Composto?), o construtor por cópia terá de invocar os clones das formas!

!!!!!!!!!Atenção ao assunto da persistência!  Convinha tratá-lo algures!

!!!!!!!!!!!!!!!!!!!Especificações de excepções: incluir?