logo
Contato | Sobre...        
rebarba rebarba

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


  
 
 
Comentários
Mateus de Paula Marques | em 26/04/2008 | #
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!
Rodrigo Strauss | website | em 28/04/2008 | #
Sobre os parâmetros do main, qualquer tutorial ou livro de C ou C++ explica isso. Esses parâmetros são os passado pela linha de comando para o programa.

O ASSERT é uma macro que dispara uma exceção caso a condição não seja satisfeita. Ou seja, se a expressão "c3.a == 50 && c3.b == 50" for falsa, o programa para no debugger e você pode ver o que deu errado.
Éverton | em 17/03/2009 | #
Rodrigo gostaria de dizer que oque sei sobre templantes aprendi aqui “e se aprendi errado é culpa sua ....... hahahhah” brincadeira.
Muito bom seus textos sempre que posso venho dar uma olhada nas novidades.
Guga =P | website | em 19/04/2010 | #
Amigo Rodrigo, vc poderia falar dos templates com parametros duplos tmb só por uma questão de completude, certo?

tipo

template<typename T1, typename T2)
void init(T1* p, const T2& value)
{
new(p) T1(value); // placement
}

int main()
{
double a;
init(&a, 2354); // aqui falharia se fosse template com parametro unico, pois o compilador n teria capacidade de inferir e forçaria o programador a designar explicitamente o tipo, exemplo: init<double>(&a, 2354);

cout << a << flush;
return 0;
}

abração e excelente material, concerteza vou muito aqui
rebarba rebarba
  ::::