Escreva uma Vez, Use com Todo Tipo — Templates de Função

Escreva uma Vez, Use com Todo Tipo — Templates de Função

Escrever a mesma função para int, double e string é o tipo de repetição que o template elimina, deduzindo o tipo a partir da chamada. O artigo trata da dedução e da instanciação, dos requisitos implícitos que o corpo impõe ao tipo, e dos custos reais: binário maior, erros verbosos e definição presa ao cabeçalho.
Linguagem C++

10 min de leitura

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 entender auto també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.

Comentários

Mais em Linguagem C++

Requisitos Explícitos — Concepts e o Fim dos Erros Ilegíveis
Requisitos Explícitos — Concepts e o Fim dos Erros Ilegíveis

Sem restrição declarada, passar um tipo inadequado a um template produz…

A Sabedoria Codificada — Core Guidelines e Código Limpo em C++
A Sabedoria Codificada — Core Guidelines e Código Limpo em C++

As Core Guidelines destilam experiência de quem projetou a linguagem, mas…

Funções Anônimas e Capturas — Lambdas de Verdade
Funções Anônimas e Capturas — Lambdas de Verdade

O que distingue uma lambda de uma função comum é a captura: ela leva consigo o…