Aula prática 11

Sumário

Objectivos

Os alunos no final desta aula deverão conhecer:

  1. A forma de definir rotinas e classes genéricas em C++.
  2. A utilidade da genericidade para a construção de código utilizável em diferentes contextos sem comprometer o princípio básico das linguagens fortemente tipificadas: é preferível um erro de compilação a um erro de fusão e é preferível um erro de fusão a um erro de execução.

Deverão também ser capazes de:

  1. Distinguir entre os casos em que se deve aplicar polimorfismo dinâmico (hierarquias de classes) dos casos em que se deve aplicar o polimorfismo paramétrico.
  2. Definir pequenas rotinas genéricas.
  3. Definir pequenas classes genéricas.

Resumo

O resumo da matéria abordada nesta aula prática pode ser consultado aqui.

Exercícios

1.a)  Escreva um procedimento troca() que troque os valores de dois inteiros passados como argumento.  Use o programa de teste troca.C disponibilizado no directório ~/POO/Aula11, que depois de completo deve escrever no ecrã

2 1

1.b)  Sobrecarregue o procedimento com uma versão para float e outra para string.  Teste os novos procedimentos retirando os comentários apropriados do final da função main().

1.c)  Converta as três versões do procedimento num único procedimento genérico.  Teste-o compilando e executando o programa sem alterar a função main().  Depois experimente parametrizar explicitamente o procedimento genérico 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 procedimento genérico troca<>() que desenvolveu provavelmente faz uso de uma variável auxiliar.  Rescreva-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 copia;
copia = ...;

Considere a classe Evento, definida no mesmo ficheiro troca.C, que guarda um valor para o instante de tempo de um evento, não possui construtor por omissão e se pode escrever num canal de saída.

Teste o procedimento genérico troca<>() com esta classe retirando os comentários apropriados do final da função main().  Ocorreu um erro de compilação?  Interprete-o e conclua acerca das restrições impostas implicitamente pelo procedimento genérico ao tipo do seu parâmetro.

* Isto não significa 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 verifica 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 C++ ou estrutura sem construtores fornecidos explicitamente, sem membros privados ou protegidos, sem classes base e sem rotinas virtuais.  Um agregado pode ser inicializado através de um inicializador, i.e., uma lista de valores entre chavetas em que cada valor serve para inicializar o correspondente atributo do agregado, por ordem de definição.  Atributos de um agregado para o qual não se forneça nenhum valor no inicializador são inicializados usando o construtor por omissão do respectivo tipo (se existir esse construtor).  Se existirem agregados dentro de outros agregados, os valores no inicializador podem consistir em inicializadores com a mesma forma.  Os parênteses desses inicializadores internos podem ser omitidos desde que contenham valores para todos os atributos do respectivo agregado.  As matrizes clássicas do C++ também são agregados, embora nesse caso as afirmações acima se refiram a elementos da matriz, e não a atributos.

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!  Mas com aviso simpático do compilador.

2.a)  Defina uma classe Matriz que represente uma matriz de 10 int.  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; // Ok.
      Matriz const mc = {0, 1, 2, ...};
      mc[2] = 10;  // deve dar erro!
  2. Indexações fora dos limites devem abortar o programa com uma mensagem de erro.  Use asserções!  I.e.:
    1. Matriz m;
      cout << m[10] << endl; // deve abortar o programa!
  3. Deve ser possível converter uma matriz num ponteiro para int.  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 int const.
  4. Deve ser possível saber a dimensão 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 programa de teste no final do ficheiro de implementação matriz.C.  Estão disponíveis em ~/POO/Aula11 os ficheiros matriz.H, matriz_impl.H e matriz.C.  Complete-os.

2.b)  Converta a classe desenvolvida numa classe genérica 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 a dimensão da matriz.  Converta também o programa de teste e teste a classe genérica desenvolvida.

2.c)  Escreva uma função modelo soma<> que permita calcular a soma dos elementos de uma qualquer matriz modelo Matriz<>.  Use o programa de teste soma.C em ~/POO/Aula11.


3.a)  Escreva uma função genérica maximo<>() para cálculo do máximo de dois valores.  Use o programa de teste maximo.C, disponibilizado em ~/POO/Aula11.

3.b)  Acrescente ao ficheiro maximo.C a definição completa da classe Evento do exercício 1.  Acrescente também as seguintes linhas à função main():

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)  Retire os comentários às últimas linhas do programa.  O código que acabou de acrescentar à função main() deveria resultar em:

1 0

Experimente compilar o programa.  Que sucedeu?  Como conseguir que este programa compile?  Será que pode continuar a usar uma função?