Templates - Parte 1
Templates
Artigo publicado em 23/06/2023, atualizado em 07/12/2023 🖂
Tags: #template
- [[#Breve história]]
- [[#O que são templates e para que servem ?]]
- [[#Instanciação]]
- [[#Instanciação Implícita]]
- [[#Instanciação Explícita]]
- [[#Polimorfismo paramétrico]]
- [[#Programação Genérica]]
- [[#Templates de função (Function Templates)]]
- [[#Especialização]]
- [[#Templates de classe (Class template)]]
- [[#Resumo]]
- [[#Sugestão de leitura]]
- [[#Ferramentas]]
- [[#Referências]]
Breve história
A programação genérica foi idealizada por Alexander Stepanov e David Musser , em 1989, da seguinte forma[1]:
Templates não estavam presentes na versão inicial C++ com classes desenvolvida por Bjarne Stroustrup. Só se tornarão disponíveis na linguagem C++, em 1990, antes que o comitê C++ fosse criado.
Em meados de 1990, Alexander Stepanov e David Musser e Meng Lee experimentaram uma implementação de C++ com conceitos genéricos. Isso resultou na implementação da biblioteca STL (Standard Template Library). Quando o comitê ISO foi criado em 1994, rapidamente foram adicionados essas especificações e a STL foi padronizada oficialmente em conjunto com a versão C++, que deu origem a famosa versão C++98.[1:1]
Cronologia das inovações tecnológicas da linguagem C++ para templates.[1:2]
| Versão | Ferramenta | Descrição |
|---|---|---|
| C++11 | Variadic Templates | Templates podem ter um número de parâmetros template variáveis |
| Templates aliases | Sempre defina sinônimos para o tipo template com ajuda da declaração usada | |
| Extern Templates | Informa o compilador para não instanciar o template pois ele será instanciado em algum outro lugar. É uma ferramenta de otimização para reduziro tempo de compilação e Code Bloat. | |
| Type traits | <type_traits> contem tipos padronizados para ajudar a identificar a categoria e as características de um tipo em tempo de compilação. [2] Exemplo: is_floating_point<X> verifica se o tipo X é do tipo ponto fluturante. is_enum<X> se X é do tipo enum, etc. | |
| C++14 | Variable templates | Variáveis agora podem ser parametrizadas por tipo específico. [3] |
| C++17 | Fold Expressions | calcula o resultado usando um operador binário sobre todos os argumentos de um parâmetro pack (com opção de valor inicial) |
| typename em parâmetros templates | A palavra reservada typename pode ser usada no lugar de class template | |
| auto for non-type template parameters | A palavra reservada auto pode ser usada para parâmetros que não sejam tipos. | |
| class template argument deduction | O compilador infereo tipo de parâmetro na medida que o objeto for inicializado. | |
| C++20 | Template lambda | lambdas podem ser templates tal como qualquer outra função regular |
| String literals as template parameters | ||
| Constraints | ||
| Concepts |
O que são templates e para que servem ?
Vamos utilizar um exemplo para explicar melhor o problema que precisa ser resolvido.
#include <iostream>
using namespace std;
int maior(int a, int b) {
return (a > b ? a : b);
}
int main()
{
int i1{ 2 }, i2{ 3 };
float f1{ 4.2 }, f2{ 3.08 };
cout << "Maior de " << i1 << " e " << i2 << ": "
<< maior(i1, i2) << endl;
}
No exemplo acima criamos uma função que retorna o maior valor entre dois números passados como parâmetros da função maior.
Suponha, agora, que você verificou a necessidade de ter que calcular também o maior valor entre dois números reais. Uma solução seria sobrecarregar outra função agora com tipo float.
#include <iostream>
using namespace std;
int maior(int a, int b) {
return (a > b ? a : b);
}
float maior(float a, float b) {
return (a > b ? a : b);
}
int main()
{
int i1{ 2 }, i2{ 3 };
float f1{ 4.2 }, f2{ 3.08 };
cout << "Maior de " << i1 << " e " << i2 << ": "
<< maior(i1, i2) << endl;
cout << "Maior de " << f1 << " e " << f2 << ": "
<< maior(f1, f2) << endl;
}
Mas, se houver necessidade também de incluir caracteres ? Teremos que adicionar uma terceira função agora com o parâmetro char (ou usar inteiro). E para dificultar mais ainda, não existe somente o tipo primitivo char, existem também short, long, long long (todos esses ainda podem ter seus correspondentes unsigned), unsigned char, unsigned short, unsigned long e unsigned long long. Existe também long double assim como outros tipos tais como int8_t, int16_t, int32_t e int64_t. E, por fim, podem existir outros tipos que podem ser comparados como bigint, Matrix, point2d, além de qualquer outro que tenha especificado a sobrecarga do operador de comparação.[1:3]
Uma alternativa, que não pretendo me alongar, apenas apresentá-la porque se trata de um recurso depreciado da linguagem C (e é possível que exista a necessidade de ter que dar manutenção em algum código monolítico legado 🦖) seria a utilização de void*. Entretanto, se trata de uma prática ruim de programação e não uma alternativa séria. Portanto, se alguma vez encontrá-lo considere a possibilidade de ter que refatorar o código.
o uso de void* se tornou desnecessário desde a introdução de std::any e std::variant na versão C++17
Portanto, percebe-se que esta abordagem introduz uma série de problemas:
- Redundância de código. Temos a mesma lógica aplicada em diferentes locais no código para diferente tipos
- Manutenibilidade. Uma mudança na lógica, implicaria a necessidade de alterar todas as funções, tornando o código mais trabalhoso e suscetível a introdução de bugs.
Funções sobrecarregadas são usadas normalmente para executar operações semelhantes que envolvem lógicas de programação distintas para tipos de dados diferentes. Porém, se a lógica dos programas e suas operações forem idênticas para os vários tipos de dados, isto poderia ser codificado mais compacta e convenientemente usando-se templates de função. [4]
Se a lógica dos programas e suas operações são idênticas para os vários tipos de dados, isto pode ser codificado mais compacta e convenientemente usando-se templates de função.
Dados os tipos dos parâmetros fornecidos nas chama da função, o compilador C++ gera automaticamente funções gabarito separadas para tratar cada tipo de chamada na forma apropriada. Deste modo, definindo-se um único gabarito da função, define-se também uma família inteira de soluções.[4:1]
Outra dúvida que pode existir é quando usar herança ou template.
A dica é usar template quando for necessário fornecer funcionalidade idêntica para diferentes tipos. Por exemplo, um algoritmo de ordenação que funcione para double, int, string, mas se a necessidade for oferecer diferentes comportamentos para tipos relacionados, use herança. Por exemplo, uma aplicação que combine diferentes figuras geométricas como círculo, quadrado, linha, etc.
Todas as definições de gabaritos de função começam com a palavra-chave template seguida por uma lista de parâmetros de tipo formais para o gabarito de função colocada entre os símbolos < e > e pode ter qualquer nome (geralmente se usa T, mas isso não é obrigatório, poderíamos aplicar a mesma regra de atribuição de nomes a variáveis). [2:1]
Esses parâmetros de tipo formal também são chamados de parâmetro de tipo template (type template parameter)[1:4]. Todo parâmetro de tipo formal é precedido pela palavra-chave typename ou pela palavra-chave class. Os parâmetros de tipo formais são tipos primitivos ou tipos definidos pelo programador, usados para especificar os tipos dos parâmetros da função, especificar o tipo de retorno da função e para declarar variáveis dentro do corpo da definição da função. [4:2]
Vejamos o seguinte exemplo:
A função maior foi declarada com o tipo formal T, que passa a ter a atribuição de tipo declarada na chamada de função. T é chamado de parâmetro de tipo template (type template parameter)
#include <iostream>
using namespace std;
template <typename T>
T maior(T a, T b) {
std::cout << "Maior entre " << a << " e " << b << ": ";
return (a > b ? a : b);
}
int main()
{
int i1{ 2 }, i2{ 3 };
maior<int>(i1, i2); // T passa a ser int !
}
Maior entre 2 e 3: 3
Se você quiser entender melhor como funciona o processo de instanciação do template, ou seja, de substituição dos parâmetros templates T por aqueles usados na definição de tipo (int, float, etc.). Para isso, experimente copiar o código acima no site C++ Insights (cppinsights.io) o compilador C++ produzirá o código abaixo resultante da pré-compilação durante o processo de instanciação da função template.
O compilador C++ produziu o código abaixo da função template do Exemplo 03
#include <iostream>
using namespace std;
template<typename T>
T maior(T a, T b)
{
/* First instantiated from: insights.cpp:12 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int maior<int>(int a, int b)
{
std::operator<<cout, "Maior entre ").operator<<(a), " e ").operator<<(b), ": ";
return (a > b ? a : b);
}
#endif
int main()
{
int i1 = {2}; // linha 20
int i2 = {3}; // linha 21
maior<int>(i1, i2);
return 0;
}
Se adicionássemos os tipos int e float na linha 20 e 21 do exemplo 04, conforme o código abaixo:
Adicionando agora os tipos int e float na linha 20 e 21 do exemplo 03
#include <iostream>
using namespace std;
template <typename T>
T maior(T a, T b) {
std::cout << "Maior entre " << a << " e " << b << ": ";
return (a > b ? a : b);
}
int main()
{
int i1{ 2 }, i2{ 3 };
float f1{ 3 }, f2{ 5 };
maior(i1, i2); // T passa a ser int !
maior(f1, f2); // T passa a ser int !
}
Na pré-compilação, o pré-compilador instanciaria o seguinte código:
O compilador C++ produziu o código abaixo da função template do exemplo 05
#include <iostream>
using namespace std;
template<typename T>
T maior(T a, T b)
{
/* First instantiated from: insights.cpp:13 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int maior<int>(int a, int b)
{
std::operator<<cout, "Maior entre ").operator<<(a), " e ").operator<<(b), ": ";
return (a > b ? a : b);
}
#endif
/* First instantiated from: insights.cpp:14 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
float maior<float>(float a, float b)
{
std::operator<<cout, "Maior entre ").operator<<(a), " e ").operator<<(b), ": ";
return (a > b ? a : b);
}
#endif
int main()
{
int i1 = {2};
int i2 = {3};
float f1 = {3};
float f2 = {5};
maior(i1, i2);
maior(f1, f2);
return 0;
}
Repare no exemplo acima que foram criadas duas funções templates especializadas comportando o tipo int e float !


A declaração do tipo é desnecessária neste exemplo, porque o compilador consegue automaticamente inferir o tipo template como int como adequado (class template argument deduction). Mas, podem existir momentos que seja necessário a fim de evitar ambiguidades.
Conceitualmente existe uma diferença em inglês de function templates (i.e., templates que geram funções) e template functions (i.e., aquelas funções geradas de funções templates). A mesma coisa se aplica para class templates e template classes. [5]
Instanciação
A instanciação de template pode ser de dois tipos
- [[#Instanciação implícita]]
- [[#Instanciação explícita]]
Instanciação Implícita
Ocorre quando o compilador instancia o template pelo tipo declarado na definição. Por exemplo,
std::array<int> arr_i;
std::array<float> arr_f;
Serão instanciados arrays com tipo template int e float conforme declarados acima.
Instanciação Explícita
As vantagem de utilizar a instanciação explícita é porque ela permite reduzir o tempo de compilação assim como o tamanho do código gerado (Code Bloat) ao prevenir a redefinição do objeto.
Instanciação explícita pode ser útil para criar instanciação de templates de classe ou função sem que seja realmente utilizada em seu código. Isso pode ser útil quando você tiver que criar uma biblioteca (lib) que usa template para distribuição. A definição de templates não-instanciados não são incluidos nos arquivos .obj ( Explicit instantiation | Microsoft Learn)
A sintaxe de uma definição de uma instanciação explícita de template é realizada da seguinte forma:
Sintaxe para class template
template class-key template-name <argument-list>
Sintaxe para função template
template return-type name <argument-list>(parameter-list); template return-type name(parameter-list);
// Mantenha no arquivo de implementação (cpp)
template class A<char>;
template class A<double>;
A sintaxe de uma declaração de uma instanciação explícita de template é realizada com auxílio da palavra reservada extern
// declare onde você pretende utilizar
extern template class A<char>;
extern template class A<double>;
Polimorfismo paramétrico
Polimorfismo paramétrico é uma forma de expandir uma linguagem tornando-a mais expressiva na medida que um tipo de dado de uma função ou classe pode ser descrito genericamente para que possa suportar valores idênticos, independentes de tipo.[7]
Isso abre novas possibilidades para explorá-los em diferentes contextos, como algoritmos que executam em tempo de compilação, geração automatizada e reuso do código. Também possibilitam checagem tardia (delayed type checking) de tipo também realizada em tempo de compilação.^STROUSTRUP,2018]
Programação Genérica
Templates de função e templates de classe permitem aos programadores especificar, com um único segmento de código, uma série inteira de funções relacionadas (sobrecarregadas) — chamadas especializações de template de função — ou uma série inteira de classes relacionadas — chamadas especializações de template de classe. Essa técnica é chamada programação genérica. [4:3].
Templates não só oferecem a possibilidade de passar tipos na forma de um parâmetro de classe ou função, mas também valores constantes, conforme abaixo:
Stack foi declarado para armazenar valores de tipo double e pode armazenar 100 valores
Stack< double, 100 > mostRecentSalesFigures;
Templates em C++ podem ser de dois tipos:
- [[#Templates de função (Function Templates)]]
- [[#Templates de classe (Class template)]]
É incorreto pensar templates de função só possa ser usado na forma template <typename T> e templates de classe com template<class T>. As palavras reservadas typename e class, ==a partir da versão ==, podem ser usadas de modo intercambeáveis. Semanticamente são idênticas para o compilador.
Conforme o comitê de padronização da linguagem C++
There is no semantic difference between class and typename in a template-parameter. (C++ Standard §13.2.2)
Neste link você pode ter acesso a diversos documentos sobre a linguagem C++ Useful resources - cppreference.com
Templates de função (Function Templates)
[export] template <typename identifier_1, …, typename identifier_n > function-declaration;
[export] template <class identifier_1, …, typename identifier_n > function-declaration;
Templates de função são apenas protótipos de funções, elas só serão implicitamente instanciadas no caso de serem utilizadas (A exceção seria a instanciação explícita).
template<typename T>
void f(ParamType param);
Uma chamada poderia ser da seguinte forma
f(expr);
Durante o processo de compilação, o compilador usa a expressão para deduzir dois tipos. Estes tipos são frequentemente diferentes, ParamType frequentemente contem adornos como const ou referências.
No livro do Scott Meyer, Effective C++. Ele oferece o seguinte exemplo, se o template for declarado da seguinte forma:
template<typename T>
void f(const T& param); // _ParamType_ is const T&
e se tivermos a seguinte chamada:
int x = 0
f(x)
T será deduzido como int e paramType será deduzido como const int&. É natural pensar que o tipo deduzido será o mesmo usado no argumento passado pela função f. Mas, nem sempre ocorre desta forma.
Scott Meyer, autor do livro Effective C++ de Scott Meyer, fez um resumo bem prático em 3 casos, que fiz questão de transcrever:
- Caso 1 - ParamType é um ponteiro ou tipo referência, mas não uma referência universal (&&).
- Caso 2 - ParamType é um referência universal
- Caso 3 - ParamType não é um ponteiro, nem uma referência.
Caso 1 - ParamType é uma referência ou um ponteiro, mas não é uma referência universal


Neste caso, a dedução de tipos funciona da seguinte forma:
- Se expr for uma referência, a referência será ignorada.
- O mesmo padrão de expr reproduzido em ParamType será utilizado para determinar T.
template<typename T>
void f(T& param);
int x = 27;
const int cx = x;
const int& rx = x;
Produziria os seguintes tipos deduzidos
f(x); // T seria int e paramType int&
f(cx); // T seria const int e ParamType const int &
f(rx); // T seria int e paramType const int &
Se alterássemos o ParamType para const, veja o que aconteceria:
template<typename T>
void f(const T& param); // paramType agora é referência para const
//------------------------
int main()
{
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T é int, paramType será const int&
f(cx); // T é int, paramType será const int&
f(rx); // T é int, paramType será const int&
}
Como era esperado, a referência foi ignorada.
O que aconteceria se param fosse um ponteiro ou um ponteiro para const.
template<typename T>
void f(T* param);
//-------------------------
int main()
{
int x = 27;
const int *px = &x;
f(&x); // T é int, paramType será int*
f(px); // T é const int, paramType será const int*
}
Caso 2 - ParamType é uma Referência Universal

As coisas se tornam menos óbvias quando temos referência unviersal. Estes parâmetros são passados como rvalue reference, mas se comportam de maneira particular para lvalue e para um rvalue.
- Se expr for um lvalue, ambos T e paramType são deduzidos para serem lvalue reference, o que é bem atípico, visto o parâmetro ser declarado com &&
- Se expr for um rvalue, a regra padrão se aplica
template<typename T>
void f(T&& param); // param é uma referência universal
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // x é um lvalue, T é int&,
// paramType será também int&
f(cx); // cx é um lvalue, T é const int&,
// paramType será também const int&
f(rx); // rx é lvalue, T é const int&,
// paramType será também const int&
f(27); // 27 é rvalue, T é int,
// paramType será int&& !!!!!!!!!
Caso 3 - ParamType não é um ponteiro, nem uma referência.

Quando paramType não for nem um ponteiro ou referência, então estaremos lidando com passagem por valor.
Isso significa que param será copiado em um novo objeto, não importa o que for passado. O fato de param ser um novo objeto, motivam que as regras que regem T também sejam deduzidas da expressão expr
- Se o tipo da expressão expr for uma referência, simplesmente, ignore a parte referencial
- Se, após o passo anterior, expr ainda for const ou volatile, ignore isso também.
template<typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T e ParamTypes são int
f(cx); // T e ParamTypes são int
f(rx); // T e ParamTypes são int
Repare que mesmo rx sendo uma referência constante, o paramType foi deduzido como int. Isso acontece porque o valor será copiado em um novo objeto independente. Segue um resumo que achei na internet:

Caso especial
O que aconteceria se o argumento fosse um const char * const ?
Neste caso, o primeiro const (* const) seria desconsiderado, mas o conteúdo ao qual ele aponta, continuaria const.
template<typename T>
void f(T param); // param ainda é passado por valor
// ParamType será const char *
const char* const ptr = "Brincando com ponteiros";
f(ptr); // expr é do tipo const char * const
Alguns detalhes adicionais
Se o compilador encontrar uma chamada a uma função template no código, ele irá produzir a função adequada, com os argumentos usados para invocá-la. Caso contrário não serão instanciados.
Veja o exemplo 07
Exemplo de um template de função que retorna o maior valor. Perceba que podemos usar tipos primitivos e até mesmo classes, como std::string. Isso só foi possível pois a função template em questão utiliza o comparador > e a classe string tem sobrecarga de operador para este comparador >
#include <iostream>
using namespace std;
template <typename T>
T maior(T a, T b) {
std::cout << "Maior entre " << a << " e " << b << ": ";
return (a > b ? a : b);
}
int main()
{
int i1{ 2 }, i2{ 3 };
char c1{ 'A' }, c2{ 'Z' };
double d1{ 2.1 }, d2{ 3.5 };
float f1{ 5.2f }, f2{ 9.3f };
std::string s1{"Isabela"}, s2{ "Luisa" };
std::cout << maior(i1, i2) << std::endl;
std::cout << maior(c1, c2) << std::endl;
std::cout << maior(d1, d2) << std::endl;
std::cout << maior(f1, f2) << std::endl;
std::cout << maior(s1, s2) << std::endl;
}
Maior entre 2 e 3: 3
Maior entre A e Z: Z
Maior entre 2.1 e 3.5: 3.5
Maior entre 5.2 e 9.3: 9.3
Maior entre Isabela e Luisa: Luisa
Algumas vezes é utilizado o nome gabarito de função ou funções generalizadas para se referir a templates de função. Estou optando por usar o termo template de função, conforme tradução do livro em português do Deitel e Stroustoup.
Especialização
Especialização nos permite customizar um determinado template para um conjunto de argumentos. Uma especialização ainda é um template, o código automático gerado pelo compilador ainda é gerado.
#include <iostream>
#include <string>
// função template 1 (ft-1)
template <class T> T minimo(T a, T b)
{
return (a < b ? a : b);
}
// função template 2 (ft-2)
template <>
std::string minimo<std::string>(
std::string a,
std::string b
) {
return (a[0] < b[0] ? a : b);
}
// função template 3 (ft-3)
template <>
int minimo<int>(int a, int b)
{
return a < b ? a : b;
}
void main() {
std::string a = "abc";
std::string b = "def";
std::cout << minimo(1, 2) << std::endl; // ft-3
std::cout << minimo(a, b) << std::endl; // ft-2
//const char* pa = "abc";
//const char* pb = "def";
//std::cout << minimo(pa, pb) << std::endl;
}
1
abc
Repare que, embora exista a função template abaixo, ela não será invocada, pois não existe nenhuma ambiguidade em tempo de compilação. No lugar dela serão chamadas as funções (ft-2) e (ft-3)
template <class T> T minimo(T a, T b)
{
return (a < b ? a : b);
}
Caso seja descomentado o código abaixo
const char* pa = "abc";
const char* pb = "def";
std::cout << minimo(pa, pb) << std::endl;
A ft-1 será invocada.
O algoritmo pelo qual o compilador decide qual função será chamada envolve duas etapas:
- Primeiramente realiza uma simples resolução de sobrecarga e entre templates não-especializados nas funções declaradas
- Se um template não-especializado for selecionado, o compilador verifica se existe uma combinação melhor para a função.
Exemplo:
#include <iostream>
// f1
void func(int a)
{
std::cout << "f1" << std::endl;
}
// f2
template<class T> void func(T a)
{
std::cout << "f2" << std::endl;
}
// f3
template<class T> void func(T a,T b)
{
std::cout << "f3" << std::endl;
}
// f4
template<> void func<int>(int a)
{
std::cout << "f4" << std::endl;
}
// f5
void func(int a, int b)
{
std::cout << "f5" << std::endl;
}
// f6
template<class T>
void func(T a, T *d)
{
std::cout << "f6" << std::endl;
}
// f7
void func(int a, double d)
{
std::cout << "f7" << std::endl;
}
// f8
void func(double a, double *d)
{
std::cout << "f8" << std::endl;
}
int main()
{
int i = 6;
double d = 8;
func<int>(1); // f4
func(1,2); // f5
func(1.0, 2.0); // f3
func("a"); // f2
func(d, &d); // f8
}
Para evitar conflitos e ambiguidades. Prefira sempre definir explicitamente qual função deverá ser invocada informando o tipo.
Veja agora este outro exemplo. Qual função será o melhor candidato para ser escolhido quando funcA for chamado na função main() ?
void f(T){} // Candidato A
template <> // Candidato B
void f(T value) {}
int main()
{
funcA(42) // Qual overload será escolhido?
}
A resposta seria a função B, mesmo o candidato A aparecendo ser um match exato. Isso porque existem umas regras que extrapolam este tutorial e que pretendo abordar em um artigo específico neste tema.
A chamada abaixo resultaria em erro de compilação, pois 1.0 é deduzido como ponto flutuante, enquanto 2 é deduzido como inteiro.

Desta forma, se for possível, informe explicitamente o tipo que deseja utilizar. Como 2.0 e 1 pertecem ao domínio do tipo double, o compilador agora considera válida a chamada abaixo.
func<double>(2.0, 1); // 👍
O mais interessante é que no Exemplo 09, mesmo quando você define o tipo, a preferência será sempre da função template. Portanto, a chamada da função abaixo não alteraria a função final invocada (f3), mesmo existindo uma versão sem template (f5).
//....
func(1,2); // f5
func<int>(1, 2); // f3 e não f5 !
func<>(1,2); // Continua sendo f3
//....
Tenha bastante atenção com as regras de conversão da linguagem C++. Um literal string em C++ é considerado const char * e não um std::string.
Caso, deseje que uma cadeia literal de caracteres seja pertencente a classe std::string, adicione um "s" ao final da cadeia
auto v = "uma cadeia de caracteres"s;
template <typename T>
T soma(T a, T b)
{
return a + b;
}
int main()
{
auto b = soma("Marco", "Polo");
}
Soma foi instanciado como const char*

Ao adicionar "s" ao final do literal std::string o compilador se encarregou em instanciar a função com tipo std::string

Templates de classe (Class template)
[export] template <typename template_parameter_list> class-declaration;
[export] template <class template_parameter_list> class-declaration;
Quando uma classe usa o conceito de class template ela passa a ser conhecida como classe genérica (generic class). A programação genérica é focada em design, implementação e algoritmos de uso geral [8].
Função template membro soma da classe Calculo
template <typename T>
class Calculo
{
public:
T soma(T const a, T const b)
{
return a + b;
}
};
int main()
{
Calculo<int> calc;
calc.soma(10, 20);
}
Veja mais esse outro exemplo
class Calculo
{
public:
template <typename T>
T soma(T const a, T const b)
{
return a + b;
}
};
int main()
{
Calculo calc;
calc.soma<int>(10, 20);
}
Explicitar <int> é redundante, pois o compilador consegue descobrir o tipo mais adequado. Por questão de clareza é sempre preferível declarar explicitamente o tipo que se deseja trabalhar.
Podemos ter também funções membros templates de classes templates.
#include <iostream>
using namespace std;
template <typename T>
class Termometro
{
public:
Termometro(T const t) :temperatura(t)
{
}
T const& get() const
{
return temperatura;
}
template <typename U>
U as() const
{
return static_cast<U>(temperatura);
}
void print() const
{
cout << temperatura << endl;
}
private:
T temperatura;
};
int main()
{
Termometro<double> a(23.0);
auto temperatura = a.get();
auto n1 = a.as<int>();
cout << n1 << endl;
//auto n2 = a.as(); // se descomentar gera erro de compilação
a.print();
}
Observe que, neste exemplo, a classe foi declarada como double e a função as realiza a conversão para o tipo desejado, que é diferente do tipo declarado na classe.
Se descomentarmos a linha 37 do exemplo 14
auto n2 = a.as(); //auto n2 = a.as(); // se descomentar gera erro de compilação
O código produzirá um erro de compilação, pois a função as é uma função membro template e o compilador não consegueria deduzir o tipo adequado.
Vantagens e desvantagens de usar templates
Algumas vantagens:[1:5]
- Templates evitam repetição de código
- Fomentam a criação de bibliotecas de algoritmos, tais como a biblioteca padrão que pode ajudar em menos código
- Código mais reduzido, generalizado e menos propenso a erros.
Algumas desvantagens:[1:6]
- A sintaxe é considerada complexa e ultrapassada
- Erros de compilação são complexos para decifrar, embora os compiladores tenham feito um enorme esforço para tentar simplificar
- Eles aumentam o tempo de compilação, pois são implementados no header. Qualquer mudança no header significará que todas as implementações que dependem deste cabeçalho precisarão serem recompilados.
Apesar de alguma aparente desvantagem, templates são um recursos excepcional da linguagem C++.
Sugestão de leitura
Se quiser se aprofundar mais sobre o tema a minha sugestão de leitura é o novo livro do Marius Bancila[1:7], publicado em 2022
Pode ser obtido na amazon por este link

Uma das referências mais completa sobre o tema é o livro C++ Templates, The complete Guide[3:1] publicado em 2017

Outros artigos sugeridos por estes mesmos autores [1:8]
- Generic Programming, David Musser, Alexander Stepanov, http://stepanovpapers.com/genprog.pdf
- A History of C++: 1979−1991, Bjarne Stroustrup, https://www.stroustrup.com/hopl2.pdf
- History of C++, https://en.cppreference.com/w/cpp/language/history
- Templates in C++ - Pros and Cons, Sergey Chepurin, https://www.codeproject.com/Articles/275063/Templates-in-Cplusplus-Pros-and-Cons
Ferramentas
Essas ferramentas ajudam-nos a entender melhor os exemplos acima.
Referências
BANCILA, M. TEMPLATE METAPROGRAMMING WITH C++ unlock the power of template metaprogramming to write... robust and efficient programs. S.l.: PACKT PUBLISHING LIMITED, 2022. ]: ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
STROUSTRUP, B. The C++ programming language. Fourth edition ed. Upper Saddle River, NJ: Addison-Wesley, 2013. ]: ↩︎ ↩︎
VANDEVOORDE, D.; JOSUTTIS, N. M.; GREGOR, D. C++ templates: the complete guide. Second edition ed. Boston: Addison-Wesley, 2018. ]: ↩︎ ↩︎
DEITEL, H.; DEITEL, P. C++20 for Programmers, 3rd Edition. Place of publication not identified: Pearson, 2020. ]: ↩︎ ↩︎ ↩︎ ↩︎
MEYERS, S. Effective modern C++: 42 specific ways to improve your use of C++11 and C++14. 1st edition ed. Sebastopol, California: O’Reilly, 2015.
1. "* function templates (i.e., templates that generate functions) and template functions (i.e., the functions generated from function templates). Ditto for class templates and template classes*."]: ↩︎GREGOIRE, M. Professional C++:
. 5. ed. Indianapolis: John Wiley and Sons, 2020. ]: ↩︎CARDELLI, L. Basic polymorphic typechecking. Science of Computer Programming, v. 8, n. 2, p. 147–172, abr. 1987.
https://doi.org/10.1016/0167-6423(87)90019-0]: ↩︎STROUSTRUP, B. A tour of C++. Second edition ed. Boston: Addison-Wesley : Pearson Education, 2018. ]: ↩︎