Templates (ThiagoAdams)
De ccppbrasil.org
Tabela de conteúdo |
Introdução
Apesar de toda biblioteca padrão usar templates muitas pessoas ainda tem uma visão muito superficial sobre este assunto, deixando assim de tirar todo o proveito desta maravilhosa característica do C++. Para muitos programores que vêm do C, a primeira vista os templates podem parecer substituíveis por macros. Na verdade os templates são muito mais poderosos e espero através deste artigo colocar à tona as possibilidades que se ampliam com a programação usando templates. Este artigo apresenta a nomeclatura básica de templates e alguns conceitos como especialização completa e especialização parcial, polices/traits, embedded type information e a técnica chamada Curiously Recurring Template Pattern.
Motivação
Por trás de qualquer ferramenta sempre existe um trabalho para ser feito. Então para iniciar vou apresentar uma pequena função e como ela seria implementada utilizando templates. A função esccolhida é chamada de swap. Esta função faz a troca de conteúdo entre duas variáveis.
void swap(int & a, int & b)
{
int temp(a);
a = b;
b = temp;
}
A função foi implementada para inteiros, mas também desejo a mesma função outros tipos. Todas as implementações tem o mesmo código em comum. Então caso venha a criar uma função para cada tipo estarei claramente gerando código duplicado. Este é um dos problemas em que os templates podem ser aplicados. Para isso nós criamos uma função template swap que possui um parâmetro template T.
template<class T>
void swap(T & a, T & b)
{
T temp(a);
a = b;
b = temp;
}
…
int i1 = 1;
int i2 = 2;
swap(a, b); // ok
double d1 = 1.0;
double d2 = 2.0;
swap(d1, d2); // ok
..
As funções template podem são instanciadas em algum ponto do fonte para um tipo T específico. Quando o tipo pode ser deduzido, por exemplo pelo tipo de argumento, a syntaxe da função é exatamente igual a qualquer outra. Caso o tipo não possa ser deduzido ou é ambiguo podemos explicitamete informar o tipo através da sintaxe: swap<type>(a1,a2);
De forma similar os templates podem ser usados para declaração de classes. Por exemplo:
template <class T>
class Array
{
T * m_pointer;
…
const T & at(unsigned int index) const
{
return m_pointer[index];
}
…
};
Neste exemplo a classe array precisa ser instanciada para o tipo T. Ela é chamada de uma classe template.
Array<int> array;
Exemplos como este são encontrados nos containers da STL. A vantagem é clara, você não precisa trabalhar de forma polimórfica com os tipos, pois o que você deseja realmente é um array de inteiros.
Especialização
Uma versão do template para um tipo em especial é chamada de especialização. Existem dois tipos de especializações a especialização completa ou explícita e a especialização parcial. Para toda especialização primeiramente é preciso um template primário com o caso geral. Podemos pegar a função template swap definida anteriormente para nossa template primário e gerar uma especialização completar para um tipo específico. Por exemplo, uma especialização para o tipo int seria:
template<>
void swap(int & a, int & b)
{
cout << “swap< int>() especialization\n”;
int temp( a);
a = b;
b = temp;
}
A especialização para int será instânciada toda vez que o argumento for do tipo inteiro. Para os outros tipo o template primário será utilizado. A especialização completa é usada para definir uma classe template para um número exato de argumentos.
Especialização Parcial
A especialização parcial é uma especilização aonde você necessita mais parâmetros para definir a sua instância do template. Neste caso a lista de parâmetros não é vazia. É possível criar uma especialização parcial para o caso de ponteiros por exemplo ou qualquer outro caso. Voltando ao exemplo da função swap, ela será usada aqui para a especialização para o tipo Array. Apenas para deixar clara a diferença, uma especialização completa para a classe Array poderia ser escrita assim:
template< >
void swap<Array<int> & a, Array<int> & b>
{
[…]
}
Neste caso a especialização completa só funcionaria para o caso de um Array<int>. Para resolver a especialização para qualquer Array de tipo T, pode ser usada a especialização parcial . Ficando assim:
template< class T >
void swap( Array<T> & a, Array<T> & b )
{
a.swap(b);
}
aonde Array::swap poderia ser definida assim:
void Array< T >::swap( Array<T> & b )
{
T * tmp( m_pointer);
m_pointer = b.m_pPointer;
b.m_pointer = tmp;
}
Agora a função swap pode ser usada em qualquer Array e a especialização tornou a função muito mais eficiente e segura para o caso da classe Array, aonde uma simples cópia do ponteiro resolve o problema. Caso o template primário fosse usado seria muito mais dispendioso criar uma cópia da classe para fazer o swap. Este fator foi o que tornou interessante esta especialização partial.
Embedded Type Information
Embedded Type Information é a capacidade de um tipo armazenar informações relevantes a si próprio. Esta capacidade está intimamente ligada aos templates pois podem fornecer a eles a informação de que necessitam para sua instância. Esta informação é colocada em forma de typedefs.
Os containers da STL são exemplos de classes com embedded type information. Pegando o vector como exemplo, encontramos nele os seguintes typedefs:
allocator_type , const_iterator, const_pointer, const_reference, const_reverse_iterator, difference_type, iterator, pointer, reference, reverse_iterator, size_type, value_type.
Estes typedefs informam o tipo de iterator, o tipo do allocator, o tipo de dado usado etc. Eles são utilizados em algorítmos genéricos. Podemos perguntar para o container “T”: Qual é o seu iterator? Qual o tipo de allocator você utiliza?
Por exemplo:
template<class T> void printAll(T & container)
{
T::iterator it = container.begin();
for ( ;it != container.end(); ++it)
{
cout << *it;
}
}
Percebam que caso a informação do tipo do iterator não estivesse declarada no container eu precisaria de um parâmentro extra na minha função template.
Traits
Algumas vezes a informação contida em um tipo não é suficiente para a implementação de um template. Outras vezes, o tipo não possui e não pode conter nenhuma informação extra, como é o caso dos tipos básicos int, double, etc. Os Traits podem ser usados nestes casos. Traits, é uma pequena classe que traz informações sobre um tipo e/ou informações de como lidar com ele. Podem ser usados em outras classes ou em algorítmos.
Exemplo: Vamos supor que eu queria percorrer um container de ponteiros e deletar todos os items:
template<class T>
void DeleteAllItems(T & container)
{
T::iterator it = container.begin();
for ( ;it != container.end(); ++it)
{
delete *it;
}
container.clear();
}
Esta função template funciona perfeitamente para um ponteiro convencional. No entanto eu posso ter o mesmo algorítmo para deletar uma lista de objetos COM. (No COM é usado um método Release da interface IUnknown ao invés do operator delete)
Neste caso preciso tratar os diferentes tipos de ponteiros. Esta informação pode ser colocada externamente com a utilização de Traits.
Um Traits que contenha informação de como deletar o ponteiro pode ser definido assim:
// caso genérico
template<class T> void DeleteItemTraits(T p)
{
delete p;
}
// especializando para o caso do IUnknown
template<> void DeleteItemTraits(IUnknown *p)
{
p->Release(); // caso de um ponteiro COM
}
E o algorítmo genérico pode ser escrito:
template<class T> void DeleteAllItems(T & container)
{
T::iterator it = container.begin();
for ( ;it != container.end(); ++it)
{
DeleteItemTraits<T::value_type>(*it);
}
container.clear();
}
O termo Police também é usado para Traits, para dar a noção de ações sobre o tipo, e não apenas informações. Poderia chamar minha função de DeletePolice por exemplo. A STL possui vários exemplos de Traits. Um destes é a implementação da std::numeric_limits. A numeric_limits possui a função max() que retorna o máximo valor que pode ser contido no tipo.
Exemplo:
cout << "The maximum value for type int is: " << numeric_limits<int>::max( ) //* << endl;
Neste caso não era possivel colocar a informação do valor de max dentro do tipo int. Por isso o conceito de traits foi usado. Traits também são usados na STL em classes como a basic_string que precisa de um Traits para tratar o caso do tipo de caractere usado (char ou wchar_t).
Curiously Recurring Template Pattern
O nome “Curiously Recurring Template Pattern” ou CRTP é empregado para um técnica que faz com que a classe base derive de outra classe cujo parâmetro template é a própria classe derivada.
Ou seja:
class derived : public base<derived>
{
…
}
Em poucas palavras, esta técnica permite que a classe base acesse a classe derivada através de um cast de seu ponteiro. A maneira mais comum da classe base acessar a classe derivada é através de funções virtuais. Com a CRTP existe uma alternativa para conseguir um comportamento semelhante sem a necessidade do overhead da vtable. Para exemplificar, vou iniciar com uma solução baseada em funções virtuais:
class base
{
virtual void do_on_change()
{
/*default code*/
}
public:
void on_change() { do_on_change(); }
};
class derived : public base
{
void do_on_change()
{
/*new code for do_on_change*/
}
};
int main()
{
derived d;
base & b = d;
b.on_change();
}
O código acima mostra o comportamento básico das funções virtuais e não requer grandes explicações. Pelo exemplo, a função do_on_change implementada na classe derivada é chamada pela classe base.
Prosseguindo, agora um exemplo equivalente utilizando a técnica CRTP.
template<class T>
class base
{
public:
void on_change()
{
T * p = static_cast<T*> this;
p->do_on_change();
}
};
class derived : public base<derived>
{
public:
void do_on_change()
{
...
}
};
int main()
{
derived d;
base<derived> & b = d;
b.do_change();
}
A chamada da função do_on_change() da classe derivada é feita diretamente pelo ponteiro “this” da classe base convertido para classe derivada.
Apesar de parecer um pouco estranho a validade do cast é garantida justamente pela herança que foi criada.
Uma das vantagens do uso desta técnica sobre a solução com funções virtuais é justamente não precisar do overhead da vtable e das chamadas de funções virtuais. Ela tem uma caracterisca estática de montagem dos tipos, enquanto as funções virtuais tem uma característica dinâmica.
Outra vantagem é que você pode acessar variáveis e enumerações da classe derivada e não apenas funções como é o caso da solução com funções virtuais.
Por exemplo:
template<class T>
class Algorithm
{
enum
{
constValue1 = 1
};
public:
void DoSomething()
{
const int value1 = static_cast<T&>(*this).constValue1;
cout << value1;
//…
}
};
class MyAlgorithm : public Algorithm<MyAlgorithm>
{
public:
enum
{
constValue1 = 2
};
};
int main()
{
MyAlgorithm a;
a.DoSomething();
return 0;
}
O “curious” do nome vem do fato da classe precisar dela mesma para existir. Isto só é possível pois os templates só existem após instanciados. (Não confunda instância de template com instância de objeto) A técnica de CRTP não substitue as funções virtuais de forma alguma, e não é equivalente em todas as situações. O uso mais geral da técnica depende da criatividade de cada um, mas é alternativa leve e interessante para muitos problemas. Vários exemplos desta técnica podem ser encontrados nas bibliotecas ATL e WTL.
Referências
- Bjarne Stroustrup, The C++ Programming Language
- Stephen C. Dewhurst, C++ Common Knowledge
