Os templates em C++

Templates são uma alternativa à sobrecarga de funções. Eles são indicados para situações em que há lógicas de programas e operações idênticas envolvidas em vários tipos de dados. Veja como utilizar os templates em C++.

Os templates fazem parte das grandes contribuições da linguagem C++ à linguagem C. Com o conceito de template, é possível passar em parâmetro os tipos e, assim, definir funções genéricas. O conceito de template vai além das funções, podendo também ser usado para as classes e estruturas.

Vantagens dos templates em C++

Denomina-se símbolo, indistintamente, uma função, uma estrutura ou uma classe. O interesse dos templates reside em sua:

- Genericidade: a partir do momento em que o tipo parâmetro fornece tudo o que é usado no símbolo template, é possível passar qualquer tipo.

- Simplicidade: só é possível codificar um símbolo, independentemente dos tipos passados em parâmetro, o que torna o código mais fácil de manter.

Desvantagens

- Como você verá mais adiante, o uso do template requer alguns cuidados (typename, etc.)

- O programa leva mais tempo para compilar.

Quando usar os templates?

O uso dos templates é particularmente relevante para definir containers, ou seja, estruturas que servem para armazenar uma coleção de objetos (uma lista, um vetor, um gráfico, etc).

Os templates também são adaptados para definir algoritmos genéricos aplicados a uma família de classe. Por exemplo: é interessante codificar um algoritmo com caminhos mais curtos, independentemente da estrutura do gráfico. Note que o uso dos functors pode ser relevante para o acesso a pesos instalados no arcos do gráfico, neste caso.

A classe do gráfico passada em parâmetro deve, então, verificar uma série de pré-requisitos para que o algoritmo seja aplicado. Se este não for o caso, o programa não será compilado...

O que colocar nos .hpp e nos .cpp

Como o C++ é uma linguagem compilada, obviamente, não se pode compilar todas as versões para um símbolo dado. Por exemplo, se definirmos uma classe template de vetor, que vou anotar como my_vector <T>, não podemos pensar em compilar my_vector <int>, my_vector <char>, my_vector <my_struct>... sabendo que há um número infinito de tipos que podem ser passados como parâmetros.

É por isso que uma classe template é (re)compilada para cada tipo de instância presente no programa. Então, se no meu programa eu uso my_vector<int> e my_vector<char>, só estas versões serão compiladas. Se, em outro programa, eu usar o my_vector my_vector<my_vector<duplo> >, eu compilarei apenas o my_vector <float> e o my_vector<my_vector<float> >. O que você deve lembrar, é que o compilador se vira para saber quais versões ele deve compilar.

Um símbolo template não pode ser "pré-compilado" porque ele é compilado para cada instância. Por isso, memorize a seguinte regra:

Se um símbolo template é usado apenas em um. cpp (arquivo de origem), ele pode ser implementado neste cpp.

Caso contrário, ele deve ser implementado em um .hpp (header).

Observação:

Às vezes, um arquivo contendo uma classe template tem uma extensão diferente dos headers (.h ou .hpp), por exemplo .tcc. Esta é apenas uma convenção de notações.

Convenção de avaliações

Em geral, os parâmetros templates são escritos com uma letra maiúscula (enquanto outros tipos são geralmente escritos em minúsculas). Na prática, pode-se fazer como desejar Entretanto, eles podem ser escritos precedidos por um T (por exemplo, Tgraph para designar um parâmetro do template representando um gráfico).

Isto pode parecer trivial, mas é bastante conveniente para entender os "typenames" e o que torna o código mais legível.

Alguns templates famosos

STL

A STL (Standard Template Library) já vem com os compiladores C++. Esta biblioteca fornece um conjunto de contêineres genéricos, principalmente:

std::vector : vetores (tabela de elementos do tipo T adjacentes na memória), acesso com O(1)

std::set : conjunto de elementos do tipo T, sem duplicatas e ordenados de acordo com o operador <, acesso com O(log(n))

std::list : listas encadeadas (acesso no O(n), inserção no início e no final da lista com O(1))

Pode-se ter uma ideia do conteúdo da STL AQUI

BGL

A BGL (Boost Graph Library) fornece classes de gráficos genéricos e os algoritmos que as acompanham (algoritmos com caminho mais curto, algoritmo de fluxo, percurso de gráfico, etc). Podemos ter uma ideia do conteúdo da BGL AQUI

Esta não está presente por padrão, mas pode ser instalada facilmente. Por exemplo, no debian:

aptitude install libboost-graph-dev

Como usar os templates

Para manipular os templates, você precisa de quatro coisas:

- A palavra-chave typename: ela indica que o tipo que segue é abstrato (parâmetro template ou depende de um parâmetro template) e que ele só deve ser considerado quando ele instancia.

- A palavra-chave template: indica que o símbolo (estrutura, classe, função) que se segue toma parâmetros templates . Escreve-se diretamente após a palavra-chave template os parâmetros templates(precedidos pela palavra-chave typename, struct, class ou type básico, onforme o tipo de parâmetro template esperado) entre chevrons (<>), seguido do símbolo escrito normalmente. Cuidado para separar corretamente os chevrons (para fechar) de modo que ele não deja confundido com o operador>>.

Veja, neste exemplo:

- como codificar uma classe template
- como codificar uma função tum operador template

Neste exemplo, os símbolos parâmetros tomam apenas um parâmetro template, mas o processo continua similar com vários parâmetros templates.

Exemplo:

template <typename T1, typename T2, ... >   
type_retorno_t minha_função(param1_t p1,param2_t p2, ...){
...
}
template <typename T1, typename T2, ... >
class minha_classe_t{
...
};
template <typename T1, typename T2, ... >
struct minha_estrutura_t{
...
};

- O operador <ita>::</ital>: com ele você pode acessar os campos (especialmente os tipos) e os métodos estáticos de uma classe ou de uma estrutura. Ele não é específico aos templates (que se aplica a classes e estruturas, em geral, e aos namespaces). Pode ser visto um pouco como o "/" dos diretórios. Asim, o std:: vector<int>::const_iterator significa que eu acesso o tipo const_iterator, armazenado na classe vector<int>, ele mesmo codificado no namespace std.

Vamos definir a nossa própria classe do vetor para ilustrar o que foi dito. É claro que, na prática, vamos utilizar diretamente a classe std::vetor da STL ...


#include <iostream>
//----------------------- início my_vector_t.hpp
#include <cstdlib>
#include <ostream>
// Uma classe template tomando um parâmetro
template <typename T>
class my_vector_t{
protected:
unsigned tamanho; // armazena lo tamanho do vetor
T *data; // armazena os componentes do vetor
público:
// O construtor
my_vector_t(unsigned taille0 = 0,const T & x0 = T()):
tamanho(tamanho0),
data((T *)malloc(sizeof(T)*tamanho0))
{
for(unsigned i=0;i<tamanho;++i) data[i] = x0;
}
// O destruidor
~my_vector_t(){
free(data);
}
// Retorna o tamanho do vetor
inline unsigned size() const{
return tamanho;
}
// Um assessor em leitura apenas na ..... casa do vetor
inline const T & operator[](unsigned i) const{
if(i >= size()) throw;
return data[i];
}
// U Um assessor em leitura escrita na ..... casa do vetor
inline T & operator[](unsigned i){
if(i >= size()) throw;
return data[i];
}
};
// Um operador template
template <typename T>
std::ostream & operator<<(std::ostream & out,const my_vector_t<T> & v){
unsigned n = v.size();
out << "[ ";
for(unsigned i=0;i<n;++i) out << v[i] << ' ';
out << ']';
return out;
}
//----------------------- fin my_vector_t.hpp
// Uma função template
template <typename T>
void escrever(const my_vector_t<T> & v){
unsigned n = v.size();
std::cout << "[ ";
for(unsigned i=0;i<n;++i) std::cout << v[i] << ' ';
std::cout << ']';
}
int main(){
my_vector_t<int> v(5); // um vetor de 5 inteiros
v[0] = 6; v[1] = 2; v[2] = 3; v[3] = 4; v[4] = 8;
ecrire<int>(v); // chamada da função template
std::cout << std::endl;
escrever(v); // chamada implícita para escrever<int>
std::cout << std::endl;
std::cout << v << std::endl; // chamada do operador template
return 0;
}

Na execução:

[ 6 2 3 4 8 ]   
[ 6 2 3 4 8 ]
[ 6 2 3 4 8 ]

Tudo o que entra "início de classe my_vector_t" e "fim da classe my_vector_t" poderia ser transferido para um header (por exemplo, my_vector.hpp) e, em seguida, incluído pelo programa principal.

Especificações dos templates

Nada impede a implementação específica de um símbolo para um conjunto de parâmetros template. Note que não há nenhuma exigência para especificar todos os parâmetros template. Neste caso, o protótipo "menos template" é o melhor para remover ambiguidades. Se repartirmos do exemplo anterior:


#include "my_vector.hpp"
// Uma especificação do template
void escrever(const my_vector_t<int> & v){
unsigned n = v.size();
std::cout << "{ ";
for(unsigned i=0;i<n;++i) std::cout << v[i] << ' ';
std::cout << '}';
}
int main(){
my_vector_t<int> v(5); // um vetor de 5 inteiros
v[0] = 6; v[1] = 2; v[2] = 3; v[3] = 4; v[4] = 8;
escrever<int>(v); // chamada da função template
std::cout << std::endl;
escrever(v); // chamada para escrever (prevalece sobre a chamada implícita para escrever<int>)
std::cout << std::endl;
std::cout << v << std::endl; // chamada do operador template
return 0;
}

Na execução:

[ 6 2 3 4 8 ]   
{ 6 2 3 4 8 }
[ 6 2 3 4 8 ]

Template por padrão

Também é possível especificar um parâmetro do template por padrão, da mesma maneira que por um parâmetro de função.

Por exemplo:

template<typename T = int>   
class my_vector_t{
//...
};
int main(){
my_vector<> v; // um vetor de int
return 0;
}

Alguns exemplos conhecidos do templates por padrão: na STL o functor de comparação, utilizado nos std::set, é inicializado por padrão pelo std::less (functor de comparação baseado em <). Assim, podemos escrever indistintamente:

std::set<int> s;   
std::set<int,std::less<int> > s_;

Recuperar as configurações templates, tipos e métodos estáticos de um a classe template

Um bom negócio com as classes templates é colocar o typedef (em público), a fim de recuperar facilmente as configurações templates. Exemplo: Eu tenho uma classe c1 <T> e quero recuperar o tipo T. Isto será possivel graças ao typedef e ao typename.

template <typename T>   
struct my_class_t{
typedef T data_t;
};
int main(){
typedef my_vector_t<int>::data_t data_t; // Este é um inteiro
}

No entanto, só podemos aplicar o operador :: se um membro da esquerda não for um tipo abstrato (ou seja, dependente de um tipo template ainda não avaliado). Por exemplo, se eu quiser manipular o typedef "const_iterator" da classe std::vector fornecida pela STL, se os parâmetros templates do std:: vector não foram afetados, o programa não compilará:

void escrever(const std::vector<int> & v){   
std::vector<int>::const_iterator vit(v.begin(),vend(v.end());
for(;vit!=vend;++vit) std::cout << *vit << ' ';
}
template <typename T>
void escrever(const std::vector<int> & v){
std::vector<T>::const_iterator vit(v.begin(),vend(v.end()); // ERRO!
for(;vit!=vend;++vit) std::cout << *vit << ' ';
}

Aqui o std::vector<T> situado à esquerda de um :: e depende de um parâmetro template. É aí que o typename entra em jogo.

template <typename T>   
void escrever(const std::vector<int> & v){
typename std::vector<T>::const_iterator vit(v.begin(),vend(v.end());
for(;vit!=vend;++vit) std::cout << *vit << ' ';
}

Vamos ressaltar que, quando o tipo à esquerda de um :: depende de um parâmetro template, ele deve ser precedido de um typename. Como os tipos se tornam rapidamente difíceis de manusear, é melhor fazer typedef. Em outro exemplo, mais complicado, daria isso:

typedef typename std::vector<typename std::vector<T>::const_iterator>::const_iterator mon_type_bizarre_t

Templates recursivos

É possível definir templates recursivos (si si !). Um exemplo:

#include <iostream>   
template <int N>
int fact(){
return N*fact<N-1>();
}
template <>
int fact<0>(){
return 1;
}
int main(){
std::cout << fact<5>() << std::endl;
return 0;
}

Aqui o interesse é bastante moderado, porque concretamente compilamos o fact<5>, fact<4> ... fact<0>, é realmente só para dar um exemplo simples de um template recursivo.

Qual o interesse de um template recursivo, então? No boost, o template recursivo permite a implementação de enuplas genéricas! Para os mais curiosos que fica no /usr/include/boost/tuple/detail/tuple_basic.hpp, então eu não direi mais nada!

Testar valores do tipo template

É possível, com o boost, verificar se um tipo template corresponde a um tipo esperado e bloquear a compilação, se necessário. Já que a biblioteca boost é utilizada, vou dar apenas um exemplo rápido:

#include <boost/type_traits/is_same.hpp>   
#include <boost/static_assert.hpp>
template <typename T>
struct minha_estrutura_que_só_deve_compilar_se_T_for_int{
BOOST_STATIC_ASSERT((boost::is_same<T,int>::value));
//...
};

Foto: © James Harrison - Unsplash

Nosso conteúdo é produzido em colaboração com especialistas em tecnologia da informação sob o comando de Jean-François Pillou, fundador do CCM.net. CCM é um site sobre tecnologia líder em nível internacional e está disponível em 11 idiomas.
Este documento, intitulado 'Os templates em C++', está disponível sob a licença Creative Commons. Você pode copiar e/ou modificar o conteúdo desta página com base nas condições estipuladas pela licença. Não se esqueça de creditar o CCM (br.ccm.net) ao utilizar este artigo.

Assine nossa newsletter!

Assine nossa newsletter!
Junte-se à comunidade