Funções template em C++ – entenda-as de uma vez por todas!

Funções template em C++

Você já se deparou com uma situação onde teve que re-escrever uma função várias vezes, mudando apenas o tipo dos parâmetros e/ou retorno da função a cada vez? Se você tem uma certa experiência com programação, provavelmente essa situação já se lhe apresentou pelo menos uma vez. Você deve saber quão tedioso é ter de reescrever a mesma função, e quão complicado é ter de manter todas essas funções atualizadas quando se deseja modificar o seu conteúdo. Pois bem, é precisamente para contornar esse problema que existem as funções template em C++.

Definição de funcão template em C++

As funções template em C++ são funções cujos parâmetros de entrada e/ou valores de retorno têm tipos genéricos, permitindo-lhes ser invocadas com diversos argumentos diferentes sem exigir que a mesma função seja reescrita para cada um dos tipos em questão.

Como criar uma função template em C++?

Uma função template tem a sua assinatura (ou seja, seu tipo de retorno, seu nome e seus parâmetros de entrada) semelhante à de uma função normal, com uma pequena diferença nos tipos de entrada e de saída. Vejamos no exemplo a seguir duas funções que fazem a mesma coisa – uma soma -, mas uma delas é uma função template e a outra uma função ordinária.

// ---------------------------------------------------
// C++ Moderno - Funções template em C++
// Exemplo #1 - Diferença entre uma função template e
//                      uma função normal
// ---------------------------------------------------

// Função "ordinária" para somar
// dois inteiros
int soma(int a, int b) {
    return a + b;
}

// Função template para somar
// dois inteiros
template<typename T>
T soma(T a, T b) {
    return a + b;
}

No exemplo #1 podemos ver uma função soma (linha 9) com dois parâmetros de entrada de tipo inteiro, a e b, que retorna a soma dos seus dois argumentos. Simples, hein? Na linha 16 vê-se uma função muito semelhante àquela da linha 9 – quase idêntica -, mas que difere daquela nos tipos de seus parâmetros de entrada e em seu tipo de retorno: eles são tipos genéricos, e essa é uma função template. Como saber, então, se uma função é um template ou não?

Qualquer um pode identificar a declaração de uma função template de um modo muito simples: esse tipo de função é sempre antecedida por template<typename T> ou template<class T>. As perguntas que se seguem, naturalmente, são estas: mas o que significam estes typename e class? E o template? E o T? Bom, vamos lá.

A palavra-chave template serve para indicar que a função que se segue é uma função template – não há surpresa aqui -, mas o que isso significa de fato? Bem, dei-lhes uma definição concisa do que são tais funções na introdução, mas agora convém falar das características principais desse tipo de função, para que se compreenda melhor a coisa. Vamos lá, então. As funções template em C++ são:

  1. parametrizadas por um ou mais parâmetros template;
  2. entidades que definem uma família de funções;

Sintaxe – funções template em C++

template<typename T>
tipoDeRetorno nomeDaFunção(T param1, T param2, …) {}

Funções template em C++ – Característica #1: Parâmetros template

Os parâmetros template de uma função template em C++ são aqueles símbolos que substituem os tipos dos parâmetros de entrada e dos valores de saída em funções normais. No exemplo #1, a função template soma possui um parâmetro template de tipo: T. Ele substitui um tipo em lugares como a declaração dos parâmetros de entrada da função; há também os parâmetros template de tipos estruturais – ou em tradução direta, que me parece horrenda, do inglês non-type template parameters: parâmetros template que não são de tipo. Desse segundo tipo de parâmetros template falarei em um outro artigo que lhes será dedicado.

Percebe-se também que o parâmetro T é precedido da palavra typename, que serve para nada mais que indicar a natureza de parâmetro template de T. Uma alternativa ao typename é o uso da palavra class: template<typename T> e template<class T> tem a mesma função. Todavia, eu prefiro o uso do typename por deixar mais claro que T é o nome de um tipo, e não dar a falsa impressão de que T se trata unicamente de tipos de classes definidas pelo usuário ou por alguma biblioteca; afinal de contas, a palavra class sugere exatamente isso, não?

Os parâmetros templates tem uma outra característica que se deve conhecer para evitar confusão no uso das funções template: eles precisam (algumas vezes) ser fornecidos como argumento da instância da função template que se deseja invocar. O que isso significa? Muito simples. Algumas vezes, quando o compilador não é capaz de deduzir automaticamente quais são os tipos dos objetos que fornecemos como argumentos da função template (ou que ela tenta retornar), é preciso ajudá-lo fornecendo o tipo real do parâmetro T para aquele uso específico do template após o nome da função, arrodeado por <> – veja a linha 24 do exemplo #2. É por essa razão que, ao usarmos o tipo vector da biblioteca padrão escrevemos o tipo dos elementos que guardaremos no vetor entre <> (como em std::vector<int> meuVetorDeInts;)

Por fim, note ainda que as funções template podem possuir vários parâmetros template, e elas podem misturar parâmetros template com parâmetros normais, mas esse será o assunto de um outro artigo – apesar de que as regras para a definição e uso de tais funções são praticamente as mesmas que aquelas em discussão neste artigo.

Funções template em C++ – Característica #2: Família de funções

Planta baixa representando a característica das funções template em C++ de produzirem uma família de funções

A segunda característica central das funções template é que elas definem uma família de funções, e não apenas uma função específica. O que isso significa? Bom, elas funcionam como plantas para a “construção” de funções similares para tipos diferentes. Como se possuíssemos o projeto arquitetônico de uma casa, e pudéssemos escolher entre vários materiais diferentes para construir suas paredes (a analogia é fraca, mas foi a melhor que encontrei por agora): madeira, alvenaria etc. Nesse exemplo, os materiais das paredes são os parâmetros template da função template.

Não é preciso de uma nova planta para se construir uma nova casa quando se deseja alterar os materiais (isso nem deve ser verdade, mas suponhamos que assim seja); de mesmo modo, não é preciso reescrever o “esqueleto” da função para que ela funcione com tipos de objetos diferentes. Um pouco confuso, eu sei, mas vejamos um exemplo para tentar esclarecer as coisas.

// ---------------------------------------------------
// C++ Moderno - Funções template em C++
// Exemplo #2 - Definição e uso de uma função template
// ---------------------------------------------------
#include <iostream>
#include <string>

using namespace std;

template <typename T>
T soma(T a, T b) {
    return a + b;
}

int main()
{
  string hello{"Hello"};
  string world{", World!"};
  // Invocação da função template soma
  // com T = std::string
  cout << soma(hello, world) << endl;;
  // Invocação da função template soma
  // com T = int
  cout << "O resultado da soma 3 + 5 eh: " << soma<int>(3, 5) << endl;
}

Saída do programa:
Hello, World!
O resultado da soma 3 + 5 eh: 8

No exemplo acima, a função soma é definida como uma função template que possui um parâmetro template: T. Esse parâmetro T é usado onde ficariam os tipos dos parâmetros de uma função normal, e também, no nosso caso, no tipo de retorno da função; ele indica que a função soma pode funcionar com quaisquer tipos que de argumentos que lhe forem passados, contanto que eles satisfaçam uma exigência: os objetos passados como argumento para a função devem possuir uma sobrecarga do operador de adicão, o operator+ (tratarei de sobrecarga de operadores em um artigo dedicado ao assunto assim que puder). Em outras palavras: se passarmos dois objetos, a e b, por exemplo, para a função soma, a operação a + b deve fazer sentido

No exemplo #2 usamos os tipos int e string. Ambos são tipos para os quais o operator+ é definido. Para os inteiros, a operação a + b retorna a soma dos dois valores inteiros (como vemos na saída do programa: O resultado da soma 3 + 5 eh: 8); para os strings, a operação a + b retorna um string cujo valor é a concatenação de a com b (por isso vemos na saída do programa Hello, World!).

Note que ao invocar a função soma para somar os inteiros 3 e 5, na linha 24, forneci explicitamente o valor do tipo T: soma<int>(3, 5). Apesar do compilador ser capaz de deduzir o tipo int automaticamente, eu posso ajudá-lo fornecendo diretamente o tipo com o qual ele deve gerar uma nova “versão” da minha função. Mas como assim, gerar uma nova versão? Era isto que eu tinha em mente quando dizia que as funções template são entidades que definem uma família de funções. Elas dão o esqueleto das funções, mas se o usuário não invocar nenhuma instância da função fornecida, ou seja, se no código não houver em lugar nenhum uma chamada da função com um tipo específico, como soma<string>(a, b), a função soma que aceita dois strings como parâmetros de entrada e retorna um string não existirá no programa final. Ainda não entendeu? Tudo bem, vejamos um exemplo para facilitar a coisa.

No exemplo #3, eu substituí a definição da função template soma por duas funções normais, uma cujos tipos de entrada e saída são int, e outra cujos tipos são string. Isso é precisamente o que o compilador faz quando ele se depara com uma função template no código-fonte do programa: ele busca invocações da função template com tipos específicos (como int, string, char, bool etc.) e gera o código da função “normal” que é idêntico ao código da função template, mas sem a linha template<typename T> e com todas as ocorrências do símbolo T substituídas pelo tipo concreto em questão – int, string, char, bool etc.

// ---------------------------------------------------
// C++ Moderno - Funções template em C++
// Exemplo #3 - Simulação da operação feita pelo compilador
//                      para gerar as instâncias da função template 
//.                      que foram invocadas.
// ---------------------------------------------------
#include <iostream>
#include <string>

using namespace std;

int soma(int a, int b) {
    return a + b;
}

string soma(string a, string b) {
    return a + b;
}

int main()
{
  string hello{"Hello"};
  string world{", World!"};
  // Invocação da função template soma
  // com T = std::string
  cout << soma(hello, world) << endl;;
  // Invocação da função template soma
  // com T = int
  cout << "O resultado da soma 3 + 5 eh: " << soma<int>(3, 5) << endl;
}

Saída do programa:
Hello, World!
O resultado da soma 3 + 5 eh: 8

Conclusão

Neste artigo tratei das funções templates em C++; do que são; de suas sintaxes; de como criá-las; das suas características principais, e forneci alguns exemplos simples de definição e uso de tais funções. Vimos que as funções template possuem parâmetros template que permitem-lhes aceitar tipos genéricos em suas entradas e saídas; que elas são entidades definidoras de famílias de funções, tornando concentrados e simples de se manter em dia códigos que seriam repetitivos e difíceis de se manter atualizados.

Além disso, sabemos agora que às vezes é preciso ajudar o compilador a determinar qual é o tipo dos objetos que passamos (ou retornamos delas) para as funções template, e que isso se fazer acrescentando ao nome da função o tipo dos dados em questão entre <>, como em soma<int>(3, 5). Na maioria das vezes, todavia, quando se tratam de tipos simples o compilador é capaz de deduzir automaticamente a quais tipos pertencem os argumentos da função.

Foto de perfil de Emanoel

Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.

Leave a Reply

Your email address will not be published. Required fields are marked *