Menu

Templates em C++: funções e classes genéricas explicadas

Escreva o código uma única vez e deixe que ele funcione com qualquer tipo usando templates de C++: templates de função, templates de classe, dedução de tipos e os confusos erros de compilação que eles causam.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Escreva uma vez, use para qualquer tipo

Na página anterior você ordenou um vector<int> com std::sort. Mas std::sort também ordena um vector<string>, um vector<double> ou um array das suas próprias structs - sem que ninguém precise escrever um sort separado para cada um. Isso não é mágica nem é sobrecarga. É um template: um único trecho de código que o compilador reaproveita para qualquer tipo que você lhe entregar.

Sem templates, você ficaria preso copiando e colando a mesma lógica para cada tipo. Aqui está a mesma função maximum escrita três vezes, exatamente a duplicação que os templates existem para eliminar:

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

Os corpos são idênticos. Só os tipos mudam. Um template permite que você diga "isto funciona para qualquer tipo T" e escreva uma só vez.

Templates de função

Você transforma uma função em um template adicionando template <typename T> na frente dela e usando T onde normalmente iria um tipo concreto.

Repare que você nunca escreveu maximum<int> nem maximum<double>. O compilador olha para os argumentos e descobre qual deve ser T - isso é a dedução de argumentos de template. Cada tipo distinto com que você a chama faz o compilador instanciar (gerar) uma função concreta separada nos bastidores.

Você pode indicar o tipo explicitamente quando a dedução não ajuda, usando colchetes angulares:

Uma armadilha comum se esconde na dedução. Como T precisa ser um único tipo, misturar tipos de argumentos a quebra:

maximum(3, 7.5);   // ERRO: T é int ou double? O compilador se recusa a adivinhar.

Você pode corrigir isso sendo explícito - maximum<double>(3, 7.5) - ou dando a cada parâmetro o seu próprio parâmetro de tipo, o que faremos a seguir.

Múltiplos parâmetros de tipo

Um template não se limita a um único tipo. Liste quantos precisar, separados por vírgulas. É assim que você escreve uma função cujos parâmetros podem ser de tipos diferentes:

Quando o tipo de retorno depende dos parâmetros, deixe o compilador resolver com auto (C++14 em diante), que combina naturalmente com templates:

Templates de classe

Os templates não servem só para funções - classes inteiras também podem ser templates. É exatamente assim que os contêineres padrão funcionam: vector<int>, o map chave-valor e pair<A, B> são todos templates de classe. Você escreve a estrutura de dados uma vez e ela armazena o tipo que você passar como parâmetro.

Aqui está uma pequena Box genérica que guarda um valor de qualquer tipo:

A diferença essencial em relação aos templates de função: com um template de classe você normalmente precisa fornecer o tipo entre colchetes angulares - Box<int> - porque nos padrões mais antigos não há argumentos de construtor dos quais deduzi-lo. (O C++17 acrescentou a dedução de argumentos de template de classe, então Box b(42); também funciona, mas ser explícito é sempre seguro e fica claro de ler.)

Os erros serão gigantescos - eis o porquê

Esta é a parte em que todo mundo tropeça, então vale a pena dizer com clareza. Um template só é totalmente verificado quando é instanciado com um tipo real. Você pode escrever um template que usa < e ele compila tranquilamente por conta própria - o erro só aparece no momento em que você o instancia com um tipo que não tem <.

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // exige que T suporte >
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// ERRO: não existe operator > para Point. A mensagem cita Point E
// reproduz esta função inteira, muitas vezes ocupando várias linhas.

Como o compilador substitui o tipo completo dentro do template e reporta as falhas a partir de dentro do código gerado, um único erro pode produzir uma parede de saída mencionando detalhes internos da biblioteca. Duas dicas de sobrevivência:

  • Leia o primeiro erro, não o último. Os erros posteriores costumam ser consequência do primeiro.
  • Procure na mensagem o nome do seu próprio tipo (aqui, Point). Isso indica qual instanciação deu errado.

A verdadeira solução é garantir que o seu tipo suporte o que o template precisa - para maximum, isso significa sobrecarregar o operator> em Point, que é assunto para uma página posterior. Os concepts do moderno C++20 podem antecipar esses erros e torná-los legíveis, mas o modelo de substituição por baixo é o mesmo.

Próximo: Classes

Você acabou de construir um template de classe Box - uma classe com dados privados, um construtor e funções membro - enquanto focava na parte dos templates. A próxima página desacelera e ensina classes como deve ser: como agrupar os dados com as funções que operam sobre eles, o que public e private realmente controlam e como as funções membro acessam o próprio estado do objeto. Templates e classes se combinam o tempo todo em C++ de verdade, então dominar bem as classes torna escrever código genérico muito mais fácil.

Perguntas frequentes

O que é um template em C++?

Um template é um molde que permite escrever uma função ou classe uma única vez e deixar que o compilador gere uma versão para cada tipo com o qual você a usa. Você escreve template <typename T> e então usa T no lugar do tipo real. O compilador produz uma versão concreta - isso se chama instanciação.

Qual é a diferença entre typename e class em um template de C++?

Em uma lista de parâmetros de template, template <typename T> e template <class T> significam exatamente a mesma coisa. Hoje em dia typename costuma ser preferido porque é mais honesto: T pode ser qualquer tipo, não apenas uma classe. A escolha da palavra-chave não tem nenhum efeito sobre o código gerado.

Por que as mensagens de erro de templates em C++ são tão longas?

Os templates são verificados quando são instanciados com um tipo real, não quando são escritos. Se um tipo não suporta uma operação que você usou (como < para ordenar), o erro aparece bem no fundo do código da biblioteca, com o tipo instanciado completo escrito por extenso, gerando páginas de saída. Leia o primeiro erro e procure nele o nome do seu tipo.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR