Rodrigo Strauss :: Blog
Tutorial de STL, parte 3/4: Ainda templates
Como prometido, nessa parte veremos como resolver o nosso problema de somar um número complexo e manter nosso código genérico.
Como dito na parte 1/2, temos basicamente duas soluções para esse problema. A primeira chama-se especialização de templates e é utilizada justamente em situações onde o template não consegue resolver o problema para todos os tipos de dados existentes. No nosso caso, o template de soma não é compatível com números complexos, já que precisamos somar a parte real e a parte imaginária separadamente. Para resolver esse problema criaremos uma especialização do template soma que contemple nosso complex_number:
template <typename T> T soma(T x, T y) { return x + y; } struct complex_number { double a; double b; }; template <> complex_number soma<complex_number>(complex_number x, complex_number y) { complex_number c; c.a = x.a + y.a; c.b = x.b + y.b; return c; } int _tmain(int argc, _TCHAR* argv[]) { complex_number c1 = {10, 20}, c2 = {40, 30}; soma<complex_number>(c1, c2); return 0; }
Nesse caso especializamos o template para tratar as particularidades do tipo complex_number. Note que a definição da especialização do template vem depois da definição da classe complex_number. Isso nos faz lembrar que essa especialização poderia ser fornecida juntamente com o header do complex_number, e não junto com o header do template soma. Dessa forma, quando você criar um tipo, você pode também fornecer junto com ele as especializações necessárias para que templates conhecidos usem o seu tipo.
Outra solução - melhor na minha opinião - é fazer com que o nosso tipo complex_number suporte o operador de soma, para que ele possa ser somado como qualquer outro número. Essa solução é melhor por tentar equalizar o tipo complex_number com os demais tipos, fazendo com que ele possa ser usado por mais funções template sem precisar especializar todas elas. Nosso código ficaria dessa forma:
template <typename T> T soma(T x, T y) { return x + y; } struct complex_number { double a; double b; complex_number operator+(const complex_number& c) { complex_number result; result.a = a + c.a; result.b = b + c.b; return result; } }; int _tmain(int argc, _TCHAR* argv[]) { complex_number c1 = {10, 20}, c2 = {40, 30}, c3; c3 = soma(c1, c2); ASSERT(c3.a == 50 && c3.b == 50); return 0; }
Nesse caso o template de soma continua exatamente o mesmo, o tipo deve suportar ser usado pelo template, e não o contrário. É importante saber esse conceito, pois ele é bastante usado pela STL. Muitos templates da STL esperam que o tipo usando suporte determinados operadores ou que tenham determinados typedefs para funcionar. Isso é o que se chama de polimosfismo em tempo de compilação: ao invés deu usar interfaces (classes abstratas) para garantir a interface (forma como um tipo se apresenta) dos objetos, isso tudo é feito somente tendo os nomes iguais. Como os templates são resolvidos em tempo de compilação, essa verificação é segura e garante que tudo funcionará perfeitamente em tempo de execução. Isso também é uma limitação, já que a resolução só pode ser feita em tempo de compilação - ao contrário do dynamic_cast.
Especialização de templates é um tópico interessante, já que é o recurso que possibilita a técnica arcana de metaprogramação, que nos permite fazer trechos dos programa que rodam em tempo de compilação. O exemplo clássico de metaprogramação usando templates é o cálculo do fatorial em tempo de compilação. Primeiro veja a implementação comum de fatorial, que é o exemplo clássico de uma função recursiva:
int fatorial(int n) { if(n == 0) return 1; else return n * fatorial(n - 1); }
Note que a recursão termina quando o valor - que diminuia a cada recursão - chega a zero. Podemos usar o mesmo conceito usando especialização de templates, já que uma especialização funciona como um if para aquela condição específica:
int fatorial(int n) { if(n == 0) return 1; else return n * fatorial(n - 1); } // template de fatorial template<int n> struct fatorial_t { enum { value = n * fatorial_t<n - 1>::value }; }; // especialização para quando chegar a zero template<> struct fatorial_t<0> { enum { value = 1 }; }; int _tmain(int argc, _TCHAR* argv[]) { int f1 = fatorial(1); int f2 = fatorial(2); int f3 = fatorial(3); int f4 = fatorial(4); f1 = fatorial_t<1>::value; f2 = fatorial_t<2>::value; f3 = fatorial_t<3>::value; f4 = fatorial_t<4>::value; return 0; }
A diferença entre fatorial e fatorial_t é que o primeiro é calculado em tempo de execução, e o segundo em tempo de compilação. Quando o programa está rodando, o valor de template_t<>::value é uma constante. E antes que você pergunte, não é necessário saber metaprogramação com templates para usar STL, só usei o tópico como um exemplo das utilidades da especialização de templates - essa sim, usada pela STL.
Acho que já vimos o que precisamos para entender a STL, no próximo post chegaremos onde realmente interessa.
Em 15/06/2006 19:52, por Rodrigo Strauss





Kra, voce poderia explicar o porque de vc ter usado parametros para main nesse exemplo?? Ou me indicar um material p/ q eu possa entender??
Tbm fiquem com duvida na linha ASSERT(c3.a == 50 && c3.b == 50);
obrigado!
ps: Parabens, seus tutos tão nervosos, hehe!