Templates - Parte 2

Templates - Parte 2

Templates necessitam de parâmetros. Parâmetros podem ser:

  1. [[#Parâmetros type template]]*
  2. [[#Parâmetros non-type template]]*
  3. Parâmetros template de template**
Parâmetro vs Argumento

Apenas ressaltar uma diferença conceitual entre parâmetros e argumentos, que será usada algumas vezes neste tutorial. Parâmetro é a definição, a variável usada na declaração da função, enquanto argumento é o valor que será passado na função.

// T, parametro1, parametro2 são todos parâmetros !
template <typename T>
T soma(T parametro1, T parametro2)
{
	return parametro1 + parametro2;
}

int main
{
	int argumento1 {3};
	int argumento2 {5};
	// int, argumento1, argumento2 são argumentos!
	int resultado = soma<int>(argumento1, argumento2);
}

Parâmetros type template

Nessa forma, os parâmetros assumem valores de tipo tais como int, float, double no processo de compilação. Você pode atribuir valores padrão para esses parâmetros conforme o exemplo abaixo.

Exemplo
//  Partes do código foram omitidas propositadamente
template <typename T = double>
class Termometro
{

	template <typename U = int>
	U as() const
	{
		return static_cast<U>(temperatura);
	}

Variadic Template (Parâmetro pack)

Na versão foi introduzido um conceito de template que aceita zero ou mais argumentos variáveis, também chamado de variadic template ou parâmetro pack[1].

Para indicar o uso de um variadic template usamos um símbolo denominado elipsis (representado por três pontinhos) após class... ou typename...

Exemplo
template <typename... T>
class wrapper { /* ... */ };

Exemplo adaptado livro [2].

template <typename T, typename... Args>
void funcao(const T &t, const Args& ... rest)
{
}
// Args é um parâmetro pack  e representa zero ou mais parâmetros
// rest é um parâmetro pack de função e representa zero ou mais parâmetros de função
int main()
{
	int i = 0; 
	double pi = 3.14; 
	string s = "Yellow submarine!";
	funcao(i, s, 42, pi);   // 3 parâmetros no pack
	funcao(s, 42, "hi");    // 2 parâmetros no pack
	funcao(pi, s);          // 1 parâmetro no pack
	funcao("hi");           // pack vazio
}

O compilador irá instancia 4 tipos de funções:

void funcao(const int&, const string&, const int&, const double&);  
void funcao(const string&, const int&, const char(&)[3]);  
void funcao(const double&, const string&);  
void funcao(const char(&)[3]);

Operador sizeof...

Quando precisamos saber quantos elementos existem no pack, utilizamos o operador sizeof... (repare a elipsis). Este operador retorna uma expressão constante e não avalia o argumento.

template<typename ... Args> 
void g(Args ... args) {  
    cout << sizeof...(Args) << endl;  // número de parâmetros  
    cout << sizeof...(args) << endl;  // número de parâmetros da função
}

Para acessar os parâmetros, precisamos expandir o pack. Para expandir um pack colocamos elipsis ao lado do padrão que queremos expandir e uma das formas é usarmos recursão para acessar os parâmetros, precisamos apenas informar uma condição de parada.

#include <iostream>
using namespace std;
// Condição de parada
template <typename T, typename... Args>
void funcao(const T& t)
{
	cout << t << endl;
}
// Função será chamada recursivamente, até a condição de parada (quando rest for vazio)
template <typename T, typename... Args>
void funcao(const T& t, const Args& ... rest)
{
	cout << t << endl;
	funcao(rest...);
}

int main()
{
	int i = 0;
	double pi = 3.14;
	string s = "Yellow submarine!";
	funcao(i, s, 42, pi);   // 3 parâmetros no pack
}
Saída produzida no console após execução

0
Yellow submarine!
42
3.14

Concepts e contraints

Na versão foi introduzido concepts e constraints. Constraints especificam requisitos para um argumento template. Um conjunto de constraints é denominado concept.

Exemplo
template <WrappableType T>
class wrapper { /* ... */ };
template <WrappableType T = int>
class wrapper { /* ... */ };
template <WrappableType... T>
class wrapper { /* ... */ };

Será visto nos próximos artigos

Parâmetros non-type template

Parâmetros non-type são aqueles que geralmente se espera quando se chama uma função ou método, ou seja: constantes, ponteiros de função ou objetos de linkagem externa ou, até mesmo, ponteiros para funções estáticas de classes, referências, std::nullptr_tautoauto&, and auto*[1:1][3].

  1. Tipos integrais
  2. Tipos de ponto flutuante
  3. Enumerações
  4. Ponteiros
  5. Ponteiros para funções membros
  6. Referências para l-values
  7. Classes, desde que:
    1. Todas as classes bases sejam públicas e não-mutáveis
    2. Todos as variáveis membros sejam non-mutable e não sejam estáticas
    3. Todos os tipos de todas as classes bases e variáveios membros sejam tipos estruturais ou arrays

A principal vantagem em usar parâmetros non-type no template em relação ao construtor são que os valores são conhecidos ainda antes do código ser compilado[3:1].

Exemplo
#include <iostream>
using namespace std;

template <typename T, size_t comprimento, size_t altura, size_t largura> 
class Malote
{
public:
	Malote();
	T volume() const ;
	T getAltura() const;
};

template <typename T, size_t comprimento, size_t altura, size_t largura>
Malote<T, comprimento, altura, largura>::Malote()
{

}

template <typename T, size_t comprimento, size_t altura, size_t largura>
T Malote<T, comprimento, altura, largura>::volume() const
{
	return comprimento * altura * largura;
}

template <typename T, size_t comprimento, size_t altura, size_t largura>
T Malote<T, comprimento, altura, largura>::getAltura() const
{
	return altura;
}

int main()
{
	size_t largura = 6;
	const size_t clargura = 6;
	constexpr size_t cexplargura = 6;
	// se descomentar o código abaixo gera erro de compilação
	// Malote<int, 2, 3, largura>  malote1; // Erro !
	Malote<int, 2, 2, clargura> malote2; // Ok!
	Malote<int, 2, 3, cexplargura> malote3; // Ok!
	Malote<int, 2, 2, cexplargura> malote4; // Ok!

	cout << "Volume malote 2:" << malote2.volume() << endl;
	cout << "Volume malote 3:" << malote3.volume() << endl;
	cout << "Altura malote 2:" << malote2.getAltura() << endl;
	// malote3 = malote4; // Erro de compilação!

}

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

Volume malote 2: 36
Volume malote 3: 36
Altura malote 2: 3

A primeira restrição é que a linha abaixo gera erro de compilação se descomentada, pois largura precisa ser constante (const ou constexpr).

Malote<int, 2, 3, largura>  malote1;

Uma segunda restrição é que como largura, altura e comprimento são parâmetros do template, eles passam a ser tipos diferentes se um dos valores for diferente.

	Malote<int, 2, 3, cexplargura> malote3; 
	Malote<int, 2, 2, cexplargura> malote4; 
	malote3 = malote4; // Erro de compilação!

Atente-se que malote3 e malote4 não tem apenas valores diferentes. Eles são tipos diferentes ! Você não pode atribuir o valor de malote4 no malote3, pois geraria erro de compilação. Verifique com exemplo abaixo

static_assertis_same_v<decltype(malote3), decltype(malote4)>;

O código acima produz erro, pois malote3 e malote4 são tipos diferentes.

Pasted image 20230625182133.png

ALERTA CODE BLOAT

Portanto, tenha atenção que a maior parte dos compiladores interpretam os parâmetros int e long como tipos diferentes. Isso pode gerar code bloat pois seriam instanciados templates de int e long, ainda que a representação na memória fosse a mesma.

Caso se depare com este problema, a solução é incorporá-lo no construtor da classe e nas variáveis membros [4].

Exemplo
int main()
{
	Malote<int> malote7; // Ok!
	Malote<long>	 malote8; // Ok!
	malote7 = malote8; //Erro, no VSStudio são tipos diferentes
}

Neste exemplo, malote7 e malote8 são instanciados como tipos diferentes pelo Microsoft Visual Studio.

Valores default (padrão)

Você também pode atribuir valores padrão. Desta forma é possível chamar a classe até sem valor algum como em malote4. Entretanto, seria necessário colocar um <> vazio.

Exemplo
// DEFINIÇÃO DA FUNÇÃO TEMPLATE
template <typename T=int, size_t comprimento=2, size_t altura=3, size_t largura=4> 
class Malote
{
public:
	Malote();
	T volume() const ;
	T getAltura() const;
};
// .. Código omitido
int main()
{
	// DECLARÇÃO DA CLASSE TEMPLATE
	Malote<> malote4; // Ok!
	//Malote	malote5; // Erro de compilação!
	Malote<int>			malote6; // Ok!
	Malote<int, 2, 2>	malote7; // Ok!

	cout << "Volume malote: " << malote4.volume() << endl;
	cout << "Volume malote: " << malote6.volume() << endl;
	cout << "Volume malote: " << malote7.volume() << endl;

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

Volume malote: 24
Volume malote: 24
Volume malote: 16

Definição vs Declaração

Definição de um template de função (ou de classe) é diferente de declaração de um template de função (ou classe).
Definição de uma função ou classe é onde será feito o protótipo, o modelo que será utilizado pela declaração.

Veja o exemplo abaixo:

Exemplo
template <typename T>  // ⬅️ Definição !
class A
{
	public:
	T t;
}; 

int main()
{
	A<int> a; // ⬅️ Declaração!
}

Existem algumas regras que precisam ser seguidas quando usamos argumentos default para parâmetros templates[5]:

  1. A ordem importa.
    1. As regras para definição de parâmetros templates são as mesmas para parâmetros de chamadas de função.
    2. As regras para declaração são que eles podem ser suprimidos da ⬅️ direita para esquerda
  2. Se um parâmetro tiver valor default, então, todos os outros após precisam ser também argumentos default.

Template auto

Na versão C++17 foi introduzido o conceito de template auto.
O tipo do template é automaticamente deduzido e se for um tipo não permitido, será gerado um erro.[1:2]

Exemplo
//Definição
template <auto x>
struct foo
{ /* … */ };
foo<42>   f1;  // foo<int>
foo<42.0> f2;  // foo<double> in C++20, error for older 
               // versions
foo<"42"> f3;  // error
// Declaração

A última linha deste código produz erro, pois, por alguma razão obscura literais strings ainda não podem ser usados como argumentos para templates non-type.[1:3]

No próximo artigo veremos parâmetros template template.

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. LIPPMAN, S. B.; LAJOIE, JOSÉE.; MOO, B. E. C++ primer. 5th ed ed. Upper Saddle River, N.J.: Addison-Wesley, 2013. ]: ↩︎

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

  4. MEYERS, S. Effective C++: 55 specific ways to improve your programs and designs. 3rd ed ed. Upper Saddle River, NJ: Addison-Wesley, 2005. ]: ↩︎

  5. GRIGORYAN, V.; WU, S. Expert C++: become a profiecient programmer by learning coding best practices with C++17 and C++20’s latest features. Birmingham: Packt Publishing, Limited, 2020. ]: ↩︎