Templates - Parte 1

Templates

Artigo publicado em 23/06/2023, atualizado em 07/12/2023 🖂
Tags: #template

Breve história

A programação genérica foi idealizada por Alexander Stepanov e David Musser , em 1989, da seguinte forma[1]:

Generic programming centers around the idea of abstracting from concrete, efficient algorithms to obtain generic algorithms that can be combined with different data representations to produce a wide variety of useful software.

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.

Exemplo
#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.

Exemplo
#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]

Warning

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.

Tip

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:

  1. Redundância de código. Temos a mesma lógica aplicada em diferentes locais no código para diferente tipos
  2. 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]

Cite

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.

Tip

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:

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 !
}
Saída produzida no console após execução

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.

Exemplo

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:

Exemplo

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:

Exemplo

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 !

Pasted image 20230619201426.png
Pasted image 20230619201454.png

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.

Note

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

  1. [[#Instanciação implícita]]
  2. [[#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 de um uma definição de instantiação explícita

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:

Exemplo

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:

  1. [[#Templates de função (Function Templates)]]
  2. [[#Templates de classe (Class template)]]
Warning

É 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++

Cite

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)

Sintaxe

[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).

Exemplo
 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 é uma referência ou um ponteiro, mas não é uma referência universal

Pasted image 20231207222352.png
Pasted image 20231207222522.png
Neste caso, a dedução de tipos funciona da seguinte forma:

  1. Se expr for uma referência, a referência será ignorada.
  2. O mesmo padrão de expr reproduzido em ParamType será utilizado para determinar T.
Exemplo caso 1-A
template<typename T>
void f(T& param);
int x = 27;             
const int cx = x;       
const int& rx = x;      

Produziria os seguintes tipos deduzidos

Exemplo caso 1-A
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:

Exemplo caso 1-B
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.

Exemplo caso 1-C
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

Pasted image 20231207222238.png
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.

Exemplo caso 2
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.

Pasted image 20231207222022.png

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

  1. Se o tipo da expressão expr for uma referência, simplesmente, ignore a parte referencial
  2. Se, após o passo anterior, expr ainda for const ou volatile, ignore isso também.
Exemplo caso 3
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:

Regras para o colapso da referência (Reference Colapsing)

Pasted image 20231207223015.png

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

A simples declaração de um template não implica em geração de código.

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

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;
}

Saída produzida no console após execução

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.

Exemplo
#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;
    
}

Saída produzida no console após execução

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:

  1. Primeiramente realiza uma simples resolução de sobrecarga e entre templates não-especializados nas funções declaradas
  2. Se um template não-especializado for selecionado, o compilador verifica se existe uma combinação melhor para a função.

Exemplo:

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.

Pasted image 20230620161948.png

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).

Exemplo
   //....
   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;

Exemplo
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*

Pasted image 20230620163052.png

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

Pasted image 20230620163204.png

Templates de classe (Class template)

Sintaxe

[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].

Exemplo

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

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.

Exemplo
#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]

  1. Templates evitam repetição de código
  2. Fomentam a criação de bibliotecas de algoritmos, tais como a biblioteca padrão que pode ajudar em menos código
  3. Código mais reduzido, generalizado e menos propenso a erros.

Algumas desvantagens:[1:6]

  1. A sintaxe é considerada complexa e ultrapassada
  2. Erros de compilação são complexos para decifrar, embora os compiladores tenham feito um enorme esforço para tentar simplificar
  3. 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

https://www.amazon.com.br/Template-Metaprogramming-everything-templates-metaprogramming/dp/1803243457/ref=sr_1_1?__mk_pt_BR=ÅMÅŽÕÑ&keywords=Template+Metaprogramming+with+C%2B%2B&qid=1687101686&sr=8-1&ufe=app_do%3Aamzn1.fos.fcd6d665-32ba-4479-9f21-b774e276a678

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

Pasted image 20230619231902.png

Outros artigos sugeridos por estes mesmos autores [1:8]

Ferramentas

Essas ferramentas ajudam-nos a entender melhor os exemplos acima.

  1. C++ Insights (cppinsights.io)
  2. Compiler Explorer (godbolt.org)

Referências


  1. BANCILA, M. TEMPLATE METAPROGRAMMING WITH C++ unlock the power of template metaprogramming to write... robust and efficient programs. S.l.: PACKT PUBLISHING LIMITED, 2022. ]: ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. STROUSTRUP, B. The C++ programming language. Fourth edition ed. Upper Saddle River, NJ: Addison-Wesley, 2013. ]: ↩︎ ↩︎

  3. VANDEVOORDE, D.; JOSUTTIS, N. M.; GREGOR, D. C++ templates: the complete guide. Second edition ed. Boston: Addison-Wesley, 2018. ]: ↩︎ ↩︎

  4. DEITEL, H.; DEITEL, P. C++20 for Programmers, 3rd Edition. Place of publication not identified: Pearson, 2020. ]: ↩︎ ↩︎ ↩︎ ↩︎

  5. 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*."]: ↩︎

  6. GREGOIRE, M. Professional C++:
    . 5. ed. Indianapolis: John Wiley and Sons, 2020. ]: ↩︎

  7. 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]: ↩︎

  8. STROUSTRUP, B. A tour of C++. Second edition ed. Boston: Addison-Wesley : Pearson Education, 2018. ]: ↩︎