No artigo Funções Anônimas e Capturas — Lambdas de Verdade desvendei as lambdas e terminei com uma pergunta que aponta para o coração da Fase 5: aquela lambda [](int x){ return x*2; } só funciona com int — e se você quisesse uma operação que servisse a qualquer tipo? Essa pergunta define a genericidade, e sua resposta são os templates, o recurso mais poderoso e característico do C++. Hoje conhecemos os templates de função: como escrever uma função uma única vez e fazê-la trabalhar com int, double, std::string — com todos os tipos, inclusive os que ainda nem existem. E há uma revelação especial aqui : os templates são o mecanismo exato sobre o qual toda a STL que você aprendeu na Fase 4 foi construída. A mágica que você usou está prestes a se abrir por dentro.
O problema: a mesma lógica repetida por tipo
Suponha uma função que devolve o maior de dois valores. Para int, você escreve uma. Para double, precisa de outra idêntica, só mudando o tipo. Para std::string, mais uma. É a sobrecarga do artigo Um Nome, Vários Sentidos — Sobrecarga de Funções — mas com um incômodo: a lógica é exatamente a mesma, e você a duplica só para trocar o tipo:
#include <iostream>
#include <string>
// Três funções idênticas em lógica, diferentes só no tipo. Duplicação pura.
int maior(int a, int b) { return a > b ? a : b; }
double maior(double a, double b) { return a > b ? a : b; }
std::string maior(std::string a, std::string b) { return a > b ? a : b; }
int main() {
std::cout << maior(3, 7) << '\n'; // 7
std::cout << maior(2.5, 1.1) << '\n'; // 2.5
std::cout << maior(std::string("ana"), std::string("bia")) << '\n'; // bia
return 0;
}
Isso funciona, mas é frágil: um bug corrigido numa versão precisa ser corrigido em todas, e cada novo tipo exige mais uma cópia. O template elimina a duplicação.
A solução: um molde parametrizado por tipo
Um template de função é um molde: você escreve a função uma vez, usando um tipo simbólico (por convenção, T), e o compilador gera automaticamente a versão concreta para cada tipo com que você a chama:
#include <iostream>
#include <string>
// 'template <typename T>' anuncia: T é um tipo a ser preenchido depois.
// A função funciona para QUALQUER T que suporte o operador >.
template <typename T>
T maior(T a, T b) {
return a > b ? a : b;
}
int main() {
std::cout << maior(3, 7) << '\n'; // T = int → 7
std::cout << maior(2.5, 1.1) << '\n'; // T = double → 2.5
std::cout << maior(std::string("ana"),
std::string("bia")) << '\n'; // T = string → bia
return 0;
}
A linha template <typename T> declara que T é um parâmetro de tipo. Dentro da função, T age como um tipo qualquer. Quando você chama maior(3, 7), o compilador deduz que T é int (pelos argumentos), e gera uma versão de maior específica para int — um processo chamado instanciação. Chame com double, e ele gera outra versão. Uma única definição, infinitas funções concretas, geradas sob demanda. E note: o compilador escolhe o tipo olhando os argumentos, então você raramente precisa dizê-lo — embora possa, com maior<double>(3, 7), forçar T = double.
O que "qualquer tipo" realmente significa: requisitos implícitos
Aqui uma sutileza honesta e importante. "Funciona com qualquer tipo" não é bem verdade — funciona com qualquer tipo que suporte as operações que a função usa. maior usa o operador >; portanto, ela funciona com qualquer T que saiba se comparar com >. Se você a chamar com um tipo que não tem >, o erro só aparece na instanciação, e a mensagem pode ser longa e confusa — uma dor tradicional dos templates:
struct Ponto { int x, y; }; // não define operator>
// maior(Ponto{1,2}, Ponto{3,4});
// ERRO: 'operator>' não existe para Ponto. A mensagem menciona a linha
// DENTRO de 'maior', não a sua chamada — pode ser difícil de decifrar.
Esse é o preço tradicional dos templates: os requisitos sobre T ficam implícitos no corpo, e violá-los gera erros tardios e verbosos. O C++20 trouxe uma cura elegante para isso — os concepts, que permitem declarar explicitamente "este template exige um T comparável" e obter erros claros. Eles são o assunto do próximo artigo; guarde a dor agora para apreciar o remédio depois.
A revelação: a STL é feita de templates
Agora a peça que fecha o círculo da Fase 4. Lembra-se de std::vector<int>, std::sort, std::find? Aqueles <int> que eu pedi para você ler como "de tal tipo" eram argumentos de template. std::vector é um template de classe (próximo artigo); std::sort é um template de função, escrito mais ou menos assim, em espírito:
// Esboço conceitual de como std::sort é declarado (simplificado):
template <typename Iterador>
void sort(Iterador inicio, Iterador fim);
// Funciona com QUALQUER iterador de acesso aleatório — de vector, de array, etc.
// Por isso o mesmo sort serve a vector<int>, vector<string>, e o que mais existir.
É por isso que std::sort funciona sobre um vector<int> e um vector<std::string> sem você escrever duas versões: ele é um template, instanciado para o tipo do seu container. Toda a generalidade da STL que você usou na Fase 4 — algoritmos que funcionam com qualquer container, containers que guardam qualquer tipo — vem daqui. Você já vinha usando templates o tempo todo; agora sabe o que estava acontecendo por baixo. A mágica era este mecanismo.
Template é um molde: o compilador deduz o tipo a partir da chamada e gera uma função concreta para cada tipo usado. O que "qualquer tipo" significa fica implícito nos requisitos que o corpo impõe — usar < exige que o tipo seja comparável, e quem passar um tipo sem esse operador recebe o erro apontando para dentro do template, não para a linha que o chamou.
Vale registrar os custos antes de sair generalizando tudo. O código é gerado para cada tipo usado, o que pode inchar o binário; os erros de compilação são notoriamente verbosos; e a definição precisa ficar inteira no cabeçalho, não separada em um .cpp, por causa de como a instanciação funciona. Nada disso desqualifica a ferramenta — a STL inteira é feita dela. A diretriz é usar template onde a generalidade ajuda de fato, e não forçar genericidade onde uma função concreta resolveria.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/language/function_template: a referência completa sobre templates de função, dedução de argumentos e instanciação.
- Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre templates: a apresentação da programação genérica pelo criador da linguagem.
- Scott Meyers, Effective Modern C++ (2014), Itens 1–4 (dedução de tipos em templates): os detalhes de como o compilador deduz
T, base para entenderautotambém. - ISO C++ Core Guidelines, seção "T" (Templates and generic programming), regras T.1 e T.2 ("Use templates to raise the level of abstraction of code"): as diretrizes sobre quando generalizar.
- cppreference.com/w/cpp/language/template_argument_deduction: os detalhes de como os argumentos de template são deduzidos a partir dos argumentos da chamada.
Exercícios
Exercício 1
Escreva um template de função T soma(T a, T b) que devolva a soma de dois valores de qualquer tipo somável. Demonstre-o com int, double e std::string (que suporta +).
Ver resposta
✓ Resposta: Template de soma:
#include <iostream>
#include <string>
template <typename T>
T soma(T a, T b) { return a + b; }
int main() {
std::cout << soma(3, 4) << '\n'; // 7
std::cout << soma(2.5, 1.5) << '\n'; // 4
std::cout << soma(std::string("ab"), std::string("cd")) << '\n'; // abcd
return 0;
}
Funciona para qualquer T que suporte + — inteiros, reais e strings (que concatenam com +).
Exercício 2
Escreva um template void troca(T& a, T& b) que troque dois valores de qualquer tipo (é essencialmente o std::swap). Explique por que os parâmetros precisam ser T&.
Ver resposta
✓ Resposta: Troca genérica:
#include <iostream>
template <typename T>
void troca(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
troca(x, y);
std::cout << x << ' ' << y << '\n'; // 2 1
return 0;
}
Os parâmetros precisam ser T& (referência) porque a função precisa modificar as variáveis do chamador — trocá-las. Se fossem por valor (T a, T b), a função trocaria apenas suas cópias locais, e as variáveis originais ficariam intactas. É a mesma razão do artigo Referências e Ponteiros, Frente a Frente, agora num contexto genérico.
Exercício 3
Explique o que o compilador faz quando encontra a chamada maior(3, 7) para o template maior<T> da aula. Use os termos "dedução" e "instanciação".
Ver resposta
✓ Resposta: Ao encontrar maior(3, 7), o compilador primeiro faz a dedução: olha os argumentos (3 e 7, ambos int) e conclui que T deve ser int. Em seguida, faz a instanciação: gera uma versão concreta da função maior com T substituído por int — efetivamente criando int maior(int a, int b) { return a > b ? a : b; } — e compila essa versão. Se você chamar maior com double em outro ponto, ele deduz T = double e instancia uma segunda versão. Cada combinação de tipos usada gera sua própria função concreta a partir do molde.
Exercício 4
O template abaixo funciona com números, mas falha ao ser chamado com um tipo sem operator<. Preveja qual é o problema, dê um exemplo de tipo que causaria o erro, e comente por que a mensagem de erro apontaria para dentro da função, não para a chamada.
template <typename T>
T minimo(T a, T b) { return a < b ? a : b; }
Ver resposta
✓ Resposta: O problema: minimo usa operator<, então só funciona com tipos que o suportem. Um tipo que causaria erro:
struct Cor { int r, g, b; }; // não define operator<
// minimo(Cor{1,2,3}, Cor{4,5,6}); // ERRO
A mensagem de erro apontaria para dentro de minimo — na linha return a < b ? a : b; — e não para a linha da chamada, porque o erro só ocorre durante a instanciação: o compilador aceita o template em si (ele é um molde válido), e só ao tentar gerar a versão para Cor descobre que a < b não compila, pois Cor não tem <. Por isso o erro "acontece" no corpo do template. Essa é a dor que os concepts (próximo artigo) resolvem, permitindo dizer de antemão "T deve ser comparável" e obter um erro claro na chamada.
Exercício 5
Explique, ligando à Fase 4, por que std::find(v.begin(), v.end(), x) consegue funcionar tanto sobre um std::vector<int> quanto sobre um std::vector<std::string> sem que ninguém escreva duas versões de find. Qual mecanismo desta aula está por trás disso?
Ver resposta
✓ Resposta: std::find funciona sobre ambos porque é um template de função, parametrizado pelo tipo do iterador (e, por consequência, pelo tipo dos elementos). Quando você o chama com v.begin()/v.end() de um vector<int>, o compilador deduz os tipos e instancia uma versão de find para iteradores de vector<int> comparando int; quando você o chama com um vector<std::string>, ele instancia outra versão para std::string. Ninguém escreveu duas versões à mão — o mecanismo desta aula, a instanciação de templates, gera cada versão concreta automaticamente a partir do molde único de find. É exatamente a generalidade que você usou por toda a Fase 4, agora explicada: a STL é um conjunto de templates, e é isso que a faz servir a qualquer tipo.