No artigo Escreva uma Vez, Use com Todo Tipo — Templates de Função aprendemos a generalizar funções sobre tipos e tivemos a revelação de que a STL é feita de templates. Mas ficou um fio à mostra: o std::vector não é uma função, é uma classe inteira que guarda qualquer tipo de elemento. Como se generaliza uma classe? Essa é a pergunta de hoje, e respondê-la fecha o entendimento de como a STL foi construída. Vamos conhecer os templates de classe e, para não ficar no abstrato, construir com as próprias mãos uma pequena estrutura genérica — uma pilha que funciona com qualquer tipo. Ao terminar, você terá visto por dentro a mesma técnica que dá vida ao std::vector<T> que você usa desde a Fase 4, e a genericidade deixará de ser mágica para virar ferramenta sua.
Generalizando uma classe inteira
A sintaxe é a mesma dos templates de função — template <typename T> — mas aplicada a uma classe. O T então pode ser usado em qualquer lugar dentro dela: como tipo de membros, de parâmetros, de retornos. Vamos construir uma Caixa genérica que guarda um valor de qualquer tipo:
#include <iostream>
#include <string>
// A classe inteira é parametrizada por T. Uma Caixa<int> guarda int,
// uma Caixa<std::string> guarda string, e assim por diante.
template <typename T>
class Caixa {
public:
Caixa(T valor) : conteudo_(valor) {}
T obter() const { return conteudo_; } // devolve o T guardado
void guardar(T novo) { conteudo_ = novo; } // substitui o T guardado
private:
T conteudo_; // o membro é do tipo genérico T
};
int main() {
Caixa<int> ci(42); // T = int
Caixa<std::string> cs("olá"); // T = std::string
std::cout << ci.obter() << '\n'; // 42
std::cout << cs.obter() << '\n'; // olá
cs.guardar("mundo");
std::cout << cs.obter() << '\n'; // mundo
return 0;
}
Diferente dos templates de função, onde o tipo era deduzido dos argumentos, num template de classe você especifica o tipo entre <> ao criar o objeto: Caixa<int>, Caixa<std::string>. Cada um desses é um tipo concreto e distinto, instanciado a partir do molde Caixa. É exatamente o que você fez em toda a Fase 4 ao escrever std::vector<int> — só que agora você sabe que estava fornecendo um argumento de template a uma classe genérica.
Construindo uma pilha genérica de verdade
Vamos a algo mais substancial, que reúne tudo do curso: uma pilha (estrutura LIFO — último a entrar, primeiro a sair) que funciona com qualquer tipo. Note como ela é construída sobre o std::vector da Fase 4, aplica a composição do artigo Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção, e generaliza com o template de hoje:
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept> // std::out_of_range
template <typename T>
class Pilha {
public:
// Empilha um elemento no topo.
void empurra(const T& valor) { // const T& : não copia à toa (Fase 1)
dados_.push_back(valor);
}
// Remove e devolve o elemento do topo.
T desempilha() {
if (vazia())
throw std::out_of_range("pilha vazia"); // exceções: Fase 6
T topo = dados_.back();
dados_.pop_back();
return topo;
}
const T& topo() const { // espia o topo sem remover
return dados_.back();
}
bool vazia() const { return dados_.empty(); }
std::size_t tamanho() const { return dados_.size(); }
private:
std::vector<T> dados_; // composição: a Pilha TEM um vector<T> (Regra do Zero!)
};
int main() {
Pilha<int> pi;
pi.empurra(1);
pi.empurra(2);
pi.empurra(3);
std::cout << "topo: " << pi.topo() << '\n'; // 3
std::cout << "removido: " << pi.desempilha() << '\n'; // 3
std::cout << "novo topo: " << pi.topo() << '\n'; // 2
Pilha<std::string> ps; // a MESMA Pilha, agora de strings
ps.empurra("a");
ps.empurra("b");
std::cout << "topo string: " << ps.topo() << '\n'; // b
return 0;
}
Repare em quanta coisa do curso converge nesta única classe. O membro é um std::vector<T> (Fase 4), o que dá à Pilha gestão de memória automática e a Regra do Zero de graça (Fase 2) — nenhum destrutor, nenhuma função de cópia a escrever. O empurra recebe const T& para não copiar à toa (Fase 1). O desempilha lança uma exceção em caso de erro (Fase 6, uma prévia). E tudo é genérico graças ao template de hoje. Esta é a demonstração viva de "cada artigo é o curso em miniatura": você já domina todas as peças, e agora as vê trabalhando juntas numa estrutura de dados real e reutilizável.
Como o std::vector é feito por dentro
Com isso, a última cortina cai. O std::vector<T> que você usa desde a Fase 4 é, em essência, um template de classe como a nossa Pilha, só que muito mais sofisticado: ele gerencia um bloco de memória bruta (em vez de delegar a outro container), implementa cuidadosamente as cinco funções especiais para cópia e movimento (artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco), controla capacidade e crescimento (artigo O Array que Cresce Sozinho — std::vector a Fundo), e oferece iteradores (artigo O Fio que Une Tudo — Iteradores como Conceito Central). Mas o esqueleto conceitual é o que você acabou de construir: uma classe parametrizada por T, com um recurso interno e métodos genéricos. Você não precisa reimplementar std::vector — ele já é excelente —, mas agora entende que não há mágica ali, apenas um template de classe bem-feito. Toda a Fase 4 se ilumina retroativamente.
A honestidade sobre parâmetros que não são tipos
Uma nota de completude, com o cuidado de sempre. Templates podem ser parametrizados não só por tipos, mas também por valores — o std::array<int, 5>, por exemplo, tem o tamanho 5 como parâmetro de template. E podem ter múltiplos parâmetros, como o std::map<Chave, Valor> que você usou, que é um template de classe com dois parâmetros de tipo. Não vamos aprofundar essas variações agora para não sobrecarregar, mas saiba que a técnica se estende: quando você viu std::map<std::string, int> na Fase 4, estava instanciando um template de classe com dois argumentos de tipo. Tudo se conecta.
Generalizar uma classe é o passo que explica a STL por dentro: um vector é um template de classe que guarda qualquer tipo e administra a própria memória. O detalhe que merece atenção é o que não foi preciso escrever — uma pilha construída sobre um std::vector<T> dispensa destrutor, cópia e atribuição, porque o membro já sabe cuidar de si. É a Regra do Zero em ação, e ela vale tanto para código genérico quanto para o concreto.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/language/class_template: a referência completa sobre templates de classe, instanciação e parâmetros.
- Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre templates: a construção de tipos genéricos pelo criador, incluindo exemplos de containers.
- cppreference.com/w/cpp/container/vector: revisitar a interface de
std::vectoragora entendendo-o como o template de classe que ele é. - Scott Meyers, Effective Modern C++ (2014): os itens sobre templates iluminam as sutilezas de instanciação de classes genéricas.
- ISO C++ Core Guidelines, seção "T", regras T.5 e T.140: diretrizes sobre projetar tipos genéricos reutilizáveis.
Exercícios
Exercício 1
Crie um template de classe Par<T> que guarde dois valores do mesmo tipo T, com métodos primeiro() e segundo() e um método troca() que inverta os dois. Demonstre com Par<int> e Par<std::string>.
Ver resposta
✓ Resposta: O template Par:
#include <iostream>
#include <string>
template <typename T>
class Par {
public:
Par(T a, T b) : primeiro_(a), segundo_(b) {}
T primeiro() const { return primeiro_; }
T segundo() const { return segundo_; }
void troca() { T tmp = primeiro_; primeiro_ = segundo_; segundo_ = tmp; }
private:
T primeiro_, segundo_;
};
int main() {
Par<int> pi(1, 2);
pi.troca();
std::cout << pi.primeiro() << ' ' << pi.segundo() << '\n'; // 2 1
Par<std::string> ps("ana", "bia");
std::cout << ps.primeiro() << ' ' << ps.segundo() << '\n'; // ana bia
return 0;
}
Exercício 2
Explique a diferença, quanto à especificação do tipo, entre chamar um template de função (maior(3, 7)) e criar um objeto de um template de classe (Caixa<int> c(42)). Por que num você não escreve <int> e no outro sim?
Ver resposta
✓ Resposta: Num template de função, o compilador deduz o tipo T a partir dos argumentos da chamada: em maior(3, 7), vê dois int e conclui T = int, então você não precisa escrever <int>. Num template de classe, não há argumentos de função no momento da criação a partir dos quais deduzir o tipo — Caixa não é chamada, é instanciada —, então você precisa dizer explicitamente qual tipo quer: Caixa<int>. funções deduzem dos argumentos; classes precisam do tipo declarado entre <>. (Nota: o C++17 introduziu dedução de argumentos de template de classe em alguns casos, mas a regra geral e mais clara é especificar o tipo.)
Exercício 3
Adicione à classe Pilha<T> da aula um método bool contem(const T& valor) const que diga se um valor está na pilha. (Dica: use std::find da Fase 4 sobre o vector interno.)
Ver resposta
✓ Resposta: O método contem:
#include <algorithm> // std::find
// dentro de Pilha<T>, na parte public:
bool contem(const T& valor) const {
return std::find(dados_.begin(), dados_.end(), valor) != dados_.end();
}
std::find procura valor no vector interno e devolve end() se não achar; comparar com end() responde se está presente — o padrão da Fase 4, agora dentro de uma classe genérica. Funciona para qualquer T que suporte == (que é o que std::find usa).
Exercício 4
Explique por que a classe Pilha<T> da aula não precisou de destrutor, construtor de cópia nem operador de atribuição, apesar de "gerenciar" uma coleção de elementos. Ligue a resposta à Regra do Zero (artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco) e ao membro std::vector<T>.
Ver resposta
✓ Resposta: A Pilha<T> não precisou de nenhuma das funções especiais porque seu único membro é um std::vector<T>, que já implementa corretamente destrutor, cópia e movimento por si só. Pela Regra do Zero (artigo Quando Copiar Dá Errado — a Regra dos Três e dos Cinco), quando todos os membros de uma classe se autogerenciam, o compilador gera automaticamente as operações certas para a classe que os contém: copiar uma Pilha copia seu vector (que faz cópia profunda), mover uma Pilha move seu vector (eficiente), e destruir uma Pilha destrói seu vector (liberando a memória). A Pilha "gerencia" elementos apenas delegando ao vector, então não há recurso cru para ela cuidar — logo, nada a escrever. É a Regra do Zero em sua forma mais pura: componha com tipos que se gerenciam e você não erra.
Exercício 5
Construa um template de classe Registrador<T> que guarde um histórico (std::vector<T>) de todos os valores que já lhe foram passados por um método registra(const T&), e ofereça um método ultimo() que devolva o valor mais recente e quantidade(). Demonstre com dois tipos diferentes e explique como esta classe reúne conceitos de várias fases do curso.
Ver resposta
✓ Resposta: O Registrador:
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
template <typename T>
class Registrador {
public:
void registra(const T& valor) { historico_.push_back(valor); } // const T& (Fase 1)
const T& ultimo() const {
if (historico_.empty()) throw std::out_of_range("nada registrado"); // Fase 6
return historico_.back();
}
std::size_t quantidade() const { return historico_.size(); }
private:
std::vector<T> historico_; // Fase 4 + Regra do Zero (Fase 2)
};
int main() {
Registrador<int> ri;
ri.registra(10); ri.registra(20);
std::cout << ri.ultimo() << " (" << ri.quantidade() << " registros)\n"; // 20 (2)
Registrador<std::string> rs;
rs.registra("primeiro"); rs.registra("segundo");
std::cout << rs.ultimo() << '\n'; // segundo
return 0;
}
Esta classe reúne várias fases: é um template de classe (Fase 5) parametrizado por T; usa std::vector<T> como membro (Fase 4), o que lhe dá a Regra do Zero (Fase 2) — sem funções especiais a escrever; recebe argumentos por const T& para evitar cópias (Fase 1); os métodos que só leem são const (Fase 3); e lança uma exceção quando não há o que devolver (Fase 6, prévia). É o curso inteiro condensado numa estrutura genérica reutilizável de vinte linhas — a prova de que os conceitos não são ilhas, mas peças de um mesmo sistema.