| FAQ Lite |
| Classes Containers e Templates |
|
 |
[ 31.1
] Como eu faço uma matriz associativa, perl-like, em C++? |
 |
[ 31.2
] Como eu posso construir um <container favorito> de objetos de diferentes tipos? |
 |
[ 31.3
] Como eu posso inserir/acessar/alterar elementos de uma lista ligada/hashtable? |
 |
[ 31.4
] Qual a idéia por trás de templates? |
 |
[ 31.5
] Qual é a sintaxe/semântica para uma template de função? |
 |
[ 31.6
] Qual a sintaxe/semântica para uma template de classe? |
 |
[ 31.7
] O que é um tipo parametrizado? |
 |
[ 31.8
] O que é genericidade? |
|
|
|
| [ 31.1 ] Como eu faço
uma matriz associativa, perl-like, em C++? |
|
| Use a classe template padrão map<Key,val>: |
|
#include <string>
#include <map>
#include <iostream>
using namespace std;
main()
{
map<string,int,less<string> > age; // age is a map from string to int
age["Fred"] = 42; // Fred is 42 years old
age["Barney"] = 37; // Barney is 37
if (todayIsFredsBirthday()) // On Fred's birthday,
++ age["Fred"]; // increment Fred's age
cout << "Fred is " << age["Fred"] << " years old\n";
}
|
|
|
| [ 31.2 ] Como eu posso
construir um <container favorito> de objetos de diferentes tipos? |
|
| Você não pode, mas pode simular isso muito facilmente.
Em C/C++ todas as matrizes são homogêneas, ou seja, os elementos têm todos o mesmo
tipo. Entretetanto, com uma camada extra de acesso indireto a memória, você pode lhes
dar a aparência de serem containers heterogêneos (um container heterogêneo é um
container que contém objetos de diferentes tipos). Há
dois casos especiais com containers heterogêneos:
O primeiro ocorre quando todos os objetos que você quer
armazenar em um container são derivados publicamente de uma classe base comum. Você pode
então declarar/definir seu container para conter pointers para a classe base. Você
armazena indiretamente um objeto de classe derivada em um container armazenando o
endereço do objeto em um elemento do container. Você pode então acessar indiretamente
os objetos do container através dos pointers (aproveitando o comportamento polimórfico).
Se você precisar saber o tipo específico de um objeto armazenado no container, você
poderá usar dynamic_cast<>
ou typeid(). Provavelmente você vai precisar do
idioma de construtuores virtuais para copiar um container
de objetos distintos.
O lado ruim dessa abordagem é que o gerenciamento de
memória torna-se um pouco mais complicado (a quem pertencem os objetos apontados? se
você delete os objetos apontados quando você destrói o container, como você
pode garantir que ninguém mais tem uma cópias desses ponters? se você não delete os
objetos apontados quando destrói o container, como você pode estar certo de alguém vai
eventualmente fazer o delete?). Essa abordagem ainda torna a processo de cópia do container
mais complexo (pode-se realmente interromper as funções de cópia do container, desde
que você não queira copiar os pointers, pelo menos não quando o container possui os
objetos apontados).
O segundo caso especial ocorre quando os tipos dos objetos
são desarticulados - não compartilham uma mesma classe base. A abordagem aqui é usar
uma handle class. O container é um recipiente de controladores de objetos (por
valor ou por pointer, a escolha é sua; por valor é mais fácil). Cada controlador de
objeto sabe como controlar (manter um pointer para) um dos objetos que você quer colocar
no container. Você pode usar tanto uma handle
class única com vários e diferentes tipos de pointers
como dados de instância, quanto uma hierarquia de handle classes que refletem os
vários tipos que você deseja armazenar, isso requer que o container seja um recipiente
de pointers da classe base (handle
class). O lado negativo dessa abordagem é obrigar a
manutenção da handle class (ou handle
classes) cada vez que você precisar alterar o conjunto
de tipos a ser armazenado no container. O benefício é que você pode usar a(s) handle class(es)
para encapsular a maior parte do trabalho de gerenciamento de memória e controle de
existência dos objetos. Usar controladores de objetos pode ser benéfico mesmo no
primeiro desses dois casos. |
|
|
|
| [ 31.3 ] Como eu posso
inserir/acessar/alterar elementos de uma lista ligada/hashtable? |
|
| Eu vou usar um caso de inserção em uma lista ligada como
um protótipo. É mais fácil permitir inserção apenas no topo ou no final da lista, mas
limitarmo-nos a isso produziria uma biblioteca muito fraca (uma biblioteca fraca é quase
pior do que nenhuma biblioteca). Essa resposta será
de dificil compreensão para novatos em C++, então eu vou dar algumas opções. A
primeira opção é a mais fácil, a segunda e a terceira são melhores.
- Reforce a List com uma localização corrente, e
funções membro tais como
advance(), backup(), atEnd(), atBegin(), getCurrElem(), setCurrElem(Elem),
insertElem(Elem) e removeElem(). Embora isso
funcione em exemplos pequenos, a noção de uma posição corrente dificulta o
acesso a elementos em duas ou mais posições da lista (por exemplo, para todos os pares x,y faça o seguinte ...)
- Remova de List as funções membro mencionadas acima, e
coloque-as em uma classe em separado, ListPosition. ListPosition funcionaria como
um posição corrente dentro da lista. Isso permite múltiplas posições dentro da mesma
lista. ListPosition seria uma friend da
class List, de modo que List pode ocultar suas partes
internas (senão as partes internas de List teriam que ser publicadas através das
funções membro public de List).
Nota: ListPosition pode usar
sobrecarga de operator para funções tais como advance() e backup(),
já que sobrecarga de operador adoça a sintaxe de funções membro normais.
- Considere toda a interação como um único evento, e crie
uma classe template para incorporar esse evento. Isso melhora a performance porque
possibilita evitar acesso a funções membro públicas (que podem ser funções virtual) durante o loop interno. Infelizmente você terá código objeto
extra no programa de aplicação, já que templates geram velocidade duplicando código.
|
|
|
|
| [ 31.4 ] Qual a idéia
por trás de templates? |
|
| Uma template é como um cortador de biscoitos que
especifica como cortar os biscoitos para que todos sejam muito parecidos (embora os
biscoitos possam ser feitos de vários tipos de massa, eles terão todos a mesma forma
básica). Da mesma forma, uma template de classe é um cortador de biscoitos para a
descrição de como construir uma família de classes de tal forma que todas se pareçam
basicamente a mesma, e uma template de função descreve como construir uma família de
funções que pareçam similares. Templates de
classes são usadas normalmente para construir container com controle de tipos, embora
isso seja muito pouco perto do que se fazer com templates. |
|
|
|
| [ 31.5 ] Qual é a
sintaxe/semântica para uma template de função? |
|
| Considere essa função que inverte seus dois argumentos
inteiros: |
|
void swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
|
| Se nós tivéssemos também que inverter float, longs, Strings, Sets e FileSystems,
ficaríamos um tanto cansados de escrever linhas de código praticamente iguais exceto
pelo tipo do dado. Repetição simples é o trabalho ideal para um computador, por isso
uma template de função: |
|
template<class T>
void swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
|
| A cada vêz que usarmos swap() com um determinado par
de tipos de dados, o compilador irá até a definição acima e criará uma outra função
template, como uma instanciação da função acima. Por exemplo: |
|
main()
{
int i,j; /*...*/ swap(i,j); // Instantiates a swap for int
float a,b; /*...*/ swap(a,b); // Instantiates a swap for float
char c,d; /*...*/ swap(c,d); // Instantiates a swap for char
String s,t; /*...*/ swap(s,t); // Instantiates a swap for String
}
|
| Nota:
Uma função template é uma instanciação de uma template de função. |
|
|
|
| [ 31.6 ] Qual a
sintaxe/semântica para uma template de classe? |
|
| Considere uma class Array cotainer que se
comporta como uma matriz de inteiros: |
|
// This would go into a header file such as "Array.h"
class Array {
public:
Array(int len=10) : len_(len), data_(new int[len]) { }
~Array() { delete [] data_; }
int len() const { return len_; }
const int& operator[](int i) const { return data_[check(i)]; }
int& operator[](int i) { return data_[check(i)]; }
Array(const Array&);
Array& operator= (const Array&);
private:
int len_;
int* data_;
int check(int i) const
{ if (i < 0 || i >= len_) throw BoundsViol("Array", i, len_);
return i; }
};
|
| Exatamente como no swap() anterior, repetir o
código acima uma vêz para matriz de float, de char, de String, matriz-de-String, etc, seria tedioso. |
|
// This would go into a header file such as "Array.h"
template<class T>
class Array {
public:
Array(int len=10) : len_(len), data_(new T[len]) { }
~Array() { delete [] data_; }
int len() const { return len_; }
const T& operator[](int i) const { return data_[check(i)]; }
T& operator[](int i) { return data_[check(i)]; }
Array(const Array<T>&);
Array<T>& operator= (const Array<T>&);
private:
int len_;
T* data_;
int check(int i) const
{ if (i < 0 || i >= len_) throw BoundsViol("Array", i, len_);
return i; }
};
|
| Diferentemente das funções template, classes templates
(instanciações de template de classe) precisam ser explícitas quanto aos parâmetros
para os quais estão sendo instanciadas. |
|
main()
{
Array<int> ai;
Array<float> af;
Array<char*> ac;
Array<String> as;
Array< Array<int> > aai;
}
|
| Repare o espaço entre os dois sinais >'s no
último exemplo. Sem esse espaço, o compilador interpretaria um >> (shift para a direita) ao invés de dois >'s |
|
|
|
| [ 31.7 ] O que é um
tipo parametrizado? |
|
| Uma outra forma de dizer template de classe. Um tipo parametrizado é um tipo que é parametrizado sobre outro
tipo ou sobre algum valor.
List<int> é um tipo (List)
parametrizado sobre um outro tipo (int). |
|
|
|
| [ 31.8 ] O que é genericidade? |
|
| Uma outra forma de dizer template de classe. Não confundir com generalidade, que significa, nesse contexto,
soluções que não sejam específicas. Genericidade
significa classes templates. |
|
|
|