Os templates em C++

Junho 2017


Templates = modelos, patões

Introdução


Vamos introduzir o conceito de template (patrão em português). Os templates fazem parte das grandes contribuições da linguagem C++ à linguagem C

Até agora, era passado em parâmetro as funções das variáveis. Com o conceito de template, é possível passar em parâmetro os tipos e, assim, definir funções genéricas. Mas o conceito de template não para nas funções, ele também pode ser usado para as classes e estruturas.

Vantagens


No que vem a seguir, chamamos 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 veremos mais tarde, 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 aplicado 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...

Que devo botar 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.

De tudo que foi dito, podemos deduzir que um símbolo template não pode ser "pré-compilados" porque ele é compilado para cada instância. Vamos memorizar 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. Pessoalmente, eu as considero como verdadeiros headers.

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, podmos fazer como quisermos. Pessoalmente, eu os escrevo precedidos por um T (por exemplo, Tgraph para designar um parâmetro do template representando um gráfico).

Isto pode parecer trivial, mas nós veremos que é bastante conveniente para entender os "typenames" e 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))

Podemos ter uma idéia 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

Primeiros passos


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


Neste exemplo vamos ver:

- 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 do 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));   
 //...   
};

Links úteis


Curso sobre C++, a parte sobre os templates está muito bem explicada e detalhada:
http://www.iis.sinica.edu.tw/~kathy/vcstl/templates.htm

Apresentação da STL (Standard Template Library): ela propõe inumeros containers templates) :
http://www.sgi.com/tech/stl/
Introdução à STL em C++ (Standard Template Library

Apresentação da BGL (Boost Graph Library): um subconjunto da biblioteca boost, que propões classes e algoritmos de gráficos templates) :
http://www.boost.org/doc/libs/1_35_0/libs/graph/doc/table_of_contents.html


Tradução feita por Lucia Maurity y Nouira

Veja também

Artigo original publicado por . Tradução feita por pintuda. Última modificação: 19 de outubro de 2011 às 18:30 por pintuda.
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.