Aula 11


1  Índice



2  Resumo

2.1  Modelos ou templates

  1. Genericidade.  O que é.
  2. Suportando genericidade com hierarquias de classes.  Objectos de classes derivadas podem ser tratados como se de objectos da classe base da hierarquia se tratassem.
  3. Exemplos em que a herança não é suficiente.
  4. Conceito de modelo de função ou procedimento (função ou procedimento modelo):
    1. Sintaxe.
    2. Utilização.
    3. Instanciação.
    4. Dedução automática de parâmetros.
  5. Conceito de classe modelo:
    1. Sintaxe.
    2. Funções e procedimentos membro também são modelos.  Sintaxe.
    3. Utilização.
    4. Instanciação.
  6. Validação implicita dos parâmetros!  Necessidade de comentários!
  7. Parâmetros por omissão.  Utilização.
  8. Notação UML para classes modelo, seus parâmetros, parâmetros por omissão, e instâncias.
  9. Construção de um modelo.  Identificação dos tipos e constantes que variam.  Passagem a parâmetros do modelo.
  10. Regras para modularização física de modelos.
[Ver guião!]

2.2  Leitura recomendada

Recomenda-se a leitura do Capítulo 6 de [1].


3  Exercícios

1.a)  Escreva um procedimento troca() que troque os valores de dois inteiros passados como argumento.  Por exemplo:
#include <iostream>

using namespace std;

// definição de troca()...

int main()
{
    int i1 = 1, i2 = 2;
    troca(i1, i2);
    cout << i1 << ' ' << i2 << endl;
}

deve escrever no ecrã
2 1
Teste o procedimento usando o programa acima.

1.b)  Sobrecarregue o procedimento com uma versão para float e outra para string.  Teste-as acrescentando código apropriado à função main().

1.c)  Converta as três versões do procedimento por um único modelo (ou template) de procedimento.  Teste-o compilando e executando o programa sem alterar a função main().  Depois experimente parametrizar explicitamente o modelo durante a invocação (i.e., troca<int>(i, j) em vez de troca(i, j)).  Que forma prefere?  Será sempre possível?

1.d)  A implementação do modelo de procedimento troca<>() que desenvolveu provavelmente faz uso de uma variável auxiliar.  Reescreva-o, se for necessário, de modo a que a definição da variável auxiliar seja separada da atribuição do seu valor *:

T aux;
aux = ...;
Crie uma classe Evento que guarda um valor para o instante de tempo do evento, não possui construtor por omissão, e se pode escrever num canal de saída:
class Evento {
  public:
    Evento(double tempo)
        : tempo_(tempo) {
    }
    double tempo() const {
        return tempo_;
    }
  private:
    double tempo_;
};

std::ostream& operator << (std::ostream& saida, Evento const& e) {
    return saida << e.tempo();
}

Acrescente à função main() um teste do modelo troca<>() usando esta classe.  Experimente compilar.  Porque ocorreu o erro?  Interprete-o e conclua acerca das restrições impostas implicitamente pelo modelo ao tipo do seu parâmetro.

* Isto não segnifica que se recomende fazê-lo, pelo contrário!  As variáveis devem ser inicializadas logo que possível com valores significativos!

2.  Pretende-se neste exercício artilhar um pouco as matrizes clássicas do C++.  Estas matrizes têm um conjunto de problemas:

  1. Não é fácil saber o seu tamanho.
  2. Não se podem passar por valor (não se podem copiar!).
  3. Não se podem atribuir directamente.
  4. Não se podem comparar directamente.
  5. Não verificam a validade dos seus índices.
Por outro lado possuem algumas características interessantes:
  1. Podem-se percorrer com ponteiros facilmente.
  2. Podem-se inicializar com inicializadores especiais.
O objectivo é, mantendo as características interessantes das matrizes clássicas do C++, eliminar todos os seus problemas.

Nota:  Em C++ chama-se "agregado" a uma classe ou estrutura sem construtores fornecidos explicitamente, sem membros privados ou protegidos, sem classes base e sem funções ou procedimentos virtuais.  As matrizes também são consideradas agregados.  Um agregado pode ser inicializado colocando uma lista de valores entre chavetas, servindo cada valor para inicializar os elementos do agregado por ordem.  Elementos de um agregado para o qual não se forneça nenhum valor no inicializador serão inicializados pelo construtor por omissão do tipo.  Se existirem agregados dentro de agregados os valores na lista podem consistir em inicializadores da mesma forma.  Os parênteses destes inicializadores internos podem ser omitidos desde que contenham valores para todos os elementos do respectivo agregado.  Por exemplo:

int m[3] = {1, 3}; // m[0] = 1, m[1] = 3 e m[2] = 0.

struct A {
    int i;
    int j;
};

A a = {-1, 4}; // a.i = -1 e a.j = 4.

struct B {
    A a;
    int m[3];
};

B b = {{10, 20}, {}}; // b.a.i = 10, b.a.j = 20, b.m[0] = 0,  b.m[1] = 0 e b.m[2] = 0.

B c = {10, 20, 0, 0, 0}; // o mesmo que b!

2.a)  Defina uma classe Matriz que represente uma matriz de 10 double.  Defina os operadores auxiliares que forem necessários.  Requisitos:
  1. Deve ser possível indexar uma matriz.  Deve ser possível atribuir um novo valor a um item obtido por indexação de uma matriz não-constante.  Se a matriz for constante tal deve ser impossível.  I.e.:
    1. Matriz m;
      m[1] = 10.0; // ok.
      Matriz const mc = {0.0, 1.1, 2.2, ...};
      mc[2] = 10;  // deve dar erro!
  2. Indexações fora dos limites devem abortar o programa com uma mensagem de erro.  Use assert!  I.e.:
    1. Matriz m;
      cout << m[10] << endl; // deve abortar o programa!
  3. Deve ser possível converter uma matriz num ponteiro para double.  O resultado deve ser um ponteiro para o primeiro elemento da matriz.  Se a matriz for constante a conversão deverá resultar num ponteiro para double const.
  4. Deve ser possível saber o tamanho de uma matriz.
  5. Deve ser possível verificar se duas matrizes são iguais ou diferentes.
  6. Deve ser possível verificar se uma matriz é menor do que outra (idem para maior, menor ou igual e maior ou igual).  Uma matriz considera-se menor que outra usando ordenação lexicográfica.  I.e.:
    1. {1, 2} < {2, 3}
      {1, 2} < {1, 3}
      {1, 2, 2} < {1, 2, 3}
Para verificar o funcionamento da classe use o seguinte programa de teste:
#include "matriz.H"

#include <iostream>

using namespace std;

int main()
{
    Matriz m = {1.1, 2.2, 3.3};

    for(int i = 0; i != m.tamanho(); ++i)
        cout << m[i] << ' ';
    cout << endl;

    for(double* i = m; i != m + m.tamanho(); ++i)
        cout << *i << ' ';
    cout << endl;

    m[0] = 4.4;

    Matriz const mc = m;

    if(m != mc)
        cout << "Falhou!  A igualdade entre matrizes está mal feita!"
             << endl;

    m[0] = 1.1;

    if(m >= mc)
        cout << "Falhou!  A comparação entre matrizes está mal feita!"
             << endl;

    for(int i = 0; i != mc.tamanho(); ++i)
        cout << mc[i] << ' ';
    cout << endl;

    for(double const* i = mc; i != mc + mc.tamanho(); ++i)
        cout << *i << ' ';
    cout << endl;

    m[1000] = 10; // Erro de execução!  Índice fora dos limites!
    mc[1] = 4.4;  // Erro de compilação!  "assignment of read-only location".
}

2.b)  Converta a classe desenvolvida para uma classe modelo Matriz<> que permita definir matrizes com qualquer número de elementos e de qualquer tipo.  O primeiro parâmetro do modelo deve ser o tipo dos elementos e o segundo deve ser o tamanho da matriz.

2.c)  Escreva uma função modelo soma<> que permita calcular a soma dos elementos de uma qualquer matriz modelo Matriz<>.  Use o seguinte programa de teste:

#include "matriz.H"

#include <iostream>
#include <string>

using namespace std;

// Defina aqui a função modelo soma<>.

int main()
{
    Matriz<string, 3> mensagens = {"Isto ", "é", " um teste marado!"};

    cout << soma(mensagens) << endl;
}

3.a)  Escreva um modelo maximo<>() para cálculo do máximo de dois valores.  Teste o modelo com o seguinte programa:
#include <iostream>

using namespace std;

// definição de maximo<>()...

int main()
{
    int i1 = 1, i2 = 2;
    cout << maximo(i1, i2) << endl;

    double d1 = 10.0, d2 = 2.0;
    cout << maximo(d1, d2) << endl;
}

3.b)  Acrescente ao programa a classe Evento do exercício 1. e o respectivo teste do modelo maximo<>():
#include <iostream>

using namespace std;

// definição de maximo<>()...

class Evento {
  public:
    Evento(double tempo)
        : tempo_(tempo) {
    }
    double tempo() const {
        return tempo_;
    }
  private:
    double tempo_;
};

std::ostream& operator << (std::ostream& saida, Evento const& e) {
    return saida << e.tempo();
}

int main()
{
    int i1 = 1, i2 = 2;
    cout << maximo(i1, i2) << endl;

    double d1 = 10.0, d2 = 2.0;
    cout << maximo(d1, d2) << endl;

    Evento e1(3.3), e2(10.0);
    cout << maximo(e1, e2) << endl;
}

Compile.  Interprete o erro obtido.  Elimine o erro definindo o operador em falta (um evento é maior que outro se tiver lugar mais cedo).  Teste de novo.

3.c)  Suponha que se pretende usar o modelo maximo<>() como se segue.

#include <iostream>

using namespace std;

// definição de maximo<>()...

int main()
{
    cout << maximo(10, 20) << endl;

    int i1 = 1, i2 = 2;
    // Pôr o maior a zero:
    maximo(i1, i2) = 0;
    cout << i1 << ' ' << i2 << endl;
}

Que deve resultar em:
20
1 0
Experimente compilar este programa.  Que sucedeu?  Como conseguir que este programa compile?


4  Referências

[1]  Michael Main e Walter Savitch, "Data Structures and Other Objects Using C++", Addison-Wesley, Reading Massachusetts, 1997. #

# Existem 10 exemplares na biblioteca do ISCTE.