Jiraspiom

Blog Pessoal

Lição 14

Lição 14 – Templates, Namespaces

Templates

Funções Template

São funções especiais que podem operar com tipos genéricos. Isto nos permite criar uma função template cuja funcionalidade pode ser adaptada a mais de um tipo ou classe sem repetir o código inteiro para cada tipo. Podemos atingir isso usando parmâmetros template. Um parâmetro template é um tipo especial de parâmetro que pode ser usado para passar um tipo como argumento: da mesma maneira que parâmetros de funções normais podem ser usados para passar valores para uma função, parâmetros template permite que passemos também tipos para uma função. Estas funções templates podem usar estes parâmetros como se fossem qualquer outro tipo comum. Seu formato é:

template <class identificador> [declaracao_da_funcao];
template <typename identificador> [declaracao_da_funcao];


OBS: A única diferença entre os dois protótipos é o uso tanto da palavra-chave class ou da palavra-chave typename. Seu uso é indistinto, já que ambas as expressões têm exatamente o mesmo significado e se comportam da mesma maneira.

Por exemplo, para criar uma função template que retorna o maior de dois objetos podemos usar:

template <class meuTipo>
meuTipo tMaior (meuTipo a, meuTipo b) {
return (a>b?a:b);
}


Aqui criamos uma função template com meuTipo como seu parâmetro template. Ele representa um tipo que ainda não foi especificado, mas pode ser usado na função template como se ele fosse um tipo comum. Como podemos ver, a função template tMaior retorna o maior de dois parâmetros deste tipo ainda indefinido.

Para usar esta função template, usamos o formato para a função de chamada:

[nome_da_funcao] <tipo> (parametros);


Por exemplo, para chamarmos tMaior para comparar dois valores inteiros do tipo int, podemos escrever:

int x,y;
tMaior <int> (x,y);


Quando o compilador encontra esta chamada à função template, ele usa o template para gerar automaticamente uma função trocando cada apararição de meuTipo pelo tipo passado como o atual parâmetro do template (no caso, int) e então chamá-lo. Por ser realizado pelo compilador, é um processo invisível ao programador.
Exemplo completo:

#include <iostream>
using namespace std;

template <class T>
T tMaior (T a, T b) {
T resultado;
resultado = (a>b)? a : b;
return (resultado);
}

int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=tMaior<int>(i,j);
n=tMaior<long>(l,m);
cout << k << endl;
cout << n << endl;
return 0;
}


, o que retorna na tela:

6
10

Neste caso, usamos T como o nome do parâmetro template ao invés de meuTipo porque é menor e na verdade é um nome comum de um parâmetro template. Entretanto, podemos usar qual identificador quisermos. Note também que usamos a função template tMaior() duas vezes. A primeira com argumentos do tipo int e a segunda com argumentos do tipo long. O compilador instanciou e então chamou cada vez a versão apropriada da função.

Como podemos ver, o tipo T é usado na função template tMaior() até para declarar novos objetos daquele tipo:

T resultado;


Assim, resultado será um objeto do mesmo tipo que os parâmetros a e b quando a função template é instanciada com um tipo específico. Neste caso específico, onde o tipo genérico T é usado como um parâmetro para tMaior o compilador pode perdeber automaticamente qual tipo de dado tem que instanciar sem ter que explicitamente especificar isso entre < e >. Poderíamos, então, ter escrito no lugar:

int i,j;
tMaior (i,j);


Já que ambos i e j são do tipo int, e o compilador pode perceber automaticamente que o parâmetro template pode ser apenas int. Este método implícito produz exatamente o mesmo resultado:

#include <iostream>
using namespace std;

template <class T>
T tMaior (T a, T b) {
return (a>b?a:b);
}

int main () {
int i=5, j=6, k;
long l=10, m=5, n;
k=tMaior(i,j);
n=tMaior(l,m);
cout << k << endl;
cout << n << endl;
return 0;
}


, o que retorna na tela:

6
10


Note como neste caso chamamos nossa função template tMaior() sem especificar explicitamente o tupo entre <>. O compilador automaticamente determina que tipo é necessário em cada chamada. Como nossa função template inclui apenas um parâmetro template (class T) e a função template em si aceita dois, ambos do tipo T, nao podemos chamar nossa função com dois objetos de diferentes tipos como argumento:

int i;
long l;
k = tMaior (i,l);


Isto seria incorreto, já que nossa função template tMaior espera dois argumentos do mesmo tipo, e neste chamada usamos dois parâmetros de tipos diferentes.

Também podemos definir funções templates que aceitam mais de um tipo de parâmetro, simplesmente especificando mais parâmetros template entre os <>. Assim:

template <class T, class U>
T tMenor (T a, U b) {
return (a<b?a:b);
}


Aqui, tMenor() aceita dois parâmetros de tipos diferentes e retorna um objeto do mesmo tipo que o primeiro parâmetro (T) que é passado. Por exemplo, após a declaração poderíamos chamar tMenor() com:

int i,j;
long l;
i = tMenor<int,long> (j,l);


ou simplesmente:

i = tMenor (j,l);


mesmo que j e l tenham tipos diferentes, já que o compilador pode determinar a instanciação apropriada de qualquer modo.

Classes templates

Ainda podemos escrever classes templates, de maneira que uma classe pode ter membros que usam parâmetros template como tipos. Por exemplo:

template <class T>
class mPar {
T valores [2];
public:
mPar (T primeiro, T segundo)
{
valores[0]=primeiro; valores[1]=segundo;
}
};


A classe que acabamos de definir serve para armazenar dois elementos de qualquer tipo válido. Por exemplo, se quiséssemos declarar um objeto desta classe para armazenar dois valores inteiros do tipo int com os valores 115 e 36, escreveríamos:

mPar<int> mObjeto (115, 36);


Esta mesma classe também seria usada para criar um objeto para armazenar qualquer outro tipo:

mPar<double> mFloats (3.0, 2.18);


A única função membro na a classe template anterior foi definida em linha com a declaração da classe em si. No caso em que definimos uma função membro fora da declaração da classe template, devemos sempre preceder aquela definição com o prefixo template <…>

#include <iostream>
using namespace std;

template <class T>
class mPar {
T a, b;
public:
mPar (T primeiro, T segundo)
{a=primeiro; b=segundo;}
T tmaior();
};

template <class T>
T mPar<T>::tmaior ()
{
T retval;
retval = a>b? a : b;
return retval;
}

int main () {
mPar <int> mObjeto (100, 75);
cout << mObjeto.tmaior();
return 0;
}


, o que retorna:

100


Note que a sintaxe da definição da função membro tmaior:

template <class T>
T mPar<T>::tmaior ()


No código, talvez podemos nos confundir com os tantos T’s, por isso explico novamente o que é cada um: o primeiro é o parâmetro template. O segundo se refere ao tipo retornado pela função. E o terceiro, entre <>, também é um requerimento, e especifica que este parâmetro da função template também é um parâmetro da classe template.

Especialização do template

Se quisermos definir uma implementação diferente para um template quando um tipo específico é passado como um parâmetro template, podemos declarar uma especialização daquele template. Por exemplo, vamos supor que temos uma classe muito simples chamada mcontainer que pode armazenar um elemento de qualquer tipo e que tem uma função membro chamada incrementa. Percebemos, entretanto, que ao armazenar um elemento do tipo char seria mais conveniente ter uma implementação completamente diferente, com uma função membro letramaiuscula, então decidimos declarar uma especialização da classe template para aquele tipo:

// especializaçao do template
#include <iostream>
using namespace std;

// classe template
template <class T>
class mcontainer {
T elemento;
public:
mcontainer (T arg) {elemento=arg;}
T incrementa () {return ++elemento;}
};

// especializacao da classe template
template <>
class mcontainer <char> {
char elemento;
public:
mcontainer (char arg) {elemento=arg;}
char letramaiuscula ()
{
if ((elemento>=’a’)&&(elemento<=’z’))
elemento+=’A’-‘a’;
return elemento;
}
};

int main () {
mcontainer<int> mint (7);
mcontainer<char> mchar (‘j’);
cout << mint.incrementa() << endl;
cout << mchar.letramaiuscula() << endl;
return 0;
}


, o que retorna na tela:

8
J


Esta é a sintaxe usada na especialização da classe template:

template <> class mcontainer <char> { … };


Note, primeiramente, que precedemos o nome da classe template com uma lista de parâmetros vazia template <>. Isto é feito para declarar explicitamente uma especialização do template. Mas, mais importante do que este prefixo é o o parâmetro de especialização <char> depois do nome da classe template. Este parâmetro de especialização por si só identifica o tipo para o qual declararemos uma especialização da classe template (char). Note as diferenças entre classes template genéricas e a especialização, respectivamente:

template <class T> class mcontainer { … };
template <> class mcontainer <char> { … };

Ao declararmos especializações para uma classe template, devemos também definir todos os seus membros, mesmo aqueles exatamente iguais à classe template genérica, pois não existe “herança” de membros do template genérico para a especialização.

Parâmetros não-tipados para templates

Apesar de argumentos de templates serem precedidos pelas palavras-chave classe ou typename, que representam tipos, templates também podem ter parâmetros tipados comuns, similarmente àqueles encontrados nas funções. Como um exemplo, vejamos esta classe template que é usada para conter seqüências de elementos:

#include <iostream>
using namespace std;

template <class T, int N>
class msequencia {
T blocomem [N];
public:
void setamembro (int x, T valor);
T tmembro (int x);
};

template <class T, int N>
void msequencia<T,N>::setamembro (int x, T valor) {
blocomem[x]=valor;
}

template <class T, int N>
T msequencia<T,N>::tmembro (int x) {
return blocomem[x];
}

int main () {
msequencia <int,5> mints;
msequencia <double,5> mfloats;
mints.setamembro (0,100);
mfloats.setamembro (3,3.1416);
cout << mints.tmembro(0) << ‘\n’;
cout << mfloats.tmembro(3) << ‘\n’;
return 0;
}


, o que retorna na tela:

100
3.1416

Também é possível setar valores ou tipos default para parâmetros de classe template. Por exemplo, se a definição da classe template anterior fosse:

template <class T=char, int N=10> class msequencia {..};


poderíamos criar objetos usando os parâmetros template padrões declarando:

msequencia<> mseq;


Que seria equivalente a:

msequencia<char,10> mseq;

Templates e arquivos de projeto múltiplos

Do ponto de vista do compilador, templates não são funções ou classes formais. Elas são compiladas por demanda, o que significa que o código de uma função template não é compilada até que uma instanciação que especifique os argumentos de template seja requerida. Neste momento, quando a instanciação é requerida, o compilador gera uma função especificamente para aqueles argumentos do template.

Quando projetos crescem é comum dividir o código de um programa em diferentes arquivos-fonte de código. Nestes casos, a interface e a implementação são geralmente separadas. Tomando como exemplo as bibliotecas de funções, a interface geralmente consiste de declarações de protótipos de todas as funções que podem ser chamadas. Estas são geralmente declaradas num “arquivo de cabeçalho” com a extensão .h, e a implementação (a definição destas funções) está em um arquivo independente, com código c++.

Como templates são compilados quando requeridos, isto força uma restrição para arquivos de projeto múltiplos: a implementação (definição) de uma classe template ou função template deve estar no mesmo arquivo que sua declaração. Isto significa que não podemos separar a interface num arquivo de cabeçalho separado, e que devemos incluir ambas interface e implementação em qualquer arquivo que utilize templates.

Já que nenhum código é gerado até que um template é instanciado quando requerido, compiladores são preparados para permitir a inclusão mais de uma vez do mesmo arquivo template com ambas as declarações e definições num projeto sem gerar erros de linkagem.

Namespaces

Namespaces permitem agrupar entidades como classes, objetos e funções sobre um nome. Assim, o escopo global pode ser dividido em sub-escopos, cada um com seu próprio nome. O formato é:

namespace identificador
{
entidades
}

Por exemplo:

namespace meuNspace
{
int a, b;
}

Para acessar as variáveis a e b de fora de meuNspace temos que usar o operador de escopo ( :: ). Por exemplo, podemos escrever:

meuNspace::a
meuNspace::b

A funcionalidade de namespaces é especialment eútil no caso em que existe a possibilidade de que um objeto ou função globais usem o mesmo identificador como algum outro, causando erros de redefinição. Por exemplo:

#include <iostream>
using namespace std;

namespace nspace1
{
int x = 5;
}

namespace nspace2
{
double x = 3.1416;
}

int main () {
cout << nspace1::x << endl;
cout << nspace2::x << endl;
return 0;
}

, o que retorna:

5
3.1416

Neste caso, existem duas variáveis globais com o mesmo nome: x. Uma é definida no namespace nspace1 e a outra no namespace nspace2. Não acontece erros de redefinição graças ao namespace.

Using

A palavra-chave using é usada para introduzir um nome de um namespace na região declarativa atual. Por exemplo:

#include <iostream>
using namespace std;

namespace nspace1
{
int x = 5;
int y = 10;
}

namespace nspace2
{
double x = 3.1416;
double y = 2.7183;
}

int main () {
using nspace1::x;
using nspace2::y;
cout << x << endl;
cout << y << endl;
cout << nspace1::y << endl;
cout << nspace2::x << endl;
return 0;
}

, o que retorna:

5
2.7183
10
3.1416

Note como neste código x (sem nenhum qualificador de nome) se refere a nspace1::x enquanto y se refere a nspace2::y, exatamente como nossa declaração do using especificou. Ainda temos acesso a nspace1::y e nspace2::x usando seus nomes completos qualificados. A palavra using também pode ser usada como uma diretiva para introduzir um namespace inteiro:

#include <iostream>
using namespace std;

namespace nspace1
{
int x = 5;
int y = 10;
}

namespace nspace2
{
double x = 3.1416;
double y = 2.7183;
}

int main () {
using namespace nspace1;
cout << x << endl;
cout << y << endl;
cout << nspace2::x << endl;
cout << nspace2::y << endl;
return 0;
}

, que retorna na tela:

5
10
3.1416
2.7183

Aqui, como declaramos que estávamos usando namespace nspace1, todas os usos diretos de x e y sem qualificadores de nome estavam se referindo à suas declarações em namespace nspace1.

Também, “using” e “using namespace” têm validade apenas no mesmo bloco em que eles são firmados ou no código inteiro se eles forem usados diretamente no escopo global. por exemplo, se tivéssemos a intenção de usar primeiramente os objetos de um namespace e então aqueles de outro namespace, poderíamos fazer algo assim:

#include <iostream>
using namespace std;

namespace nspace1
{
int x = 5;
}

namespace nspace2
{
double x = 3.1416;
}

int main () {
{
using namespace nspace1;
cout << x << endl;
}
{
using namespace nspace2;
cout << x << endl;
}
return 0;
}

, o que retorna:

5
3.1416

Namespace alias

Podemos declarar nomes alternados para namespaces existentes de acordo com o formato:

namespace novo_nome = nome_atual;

Namespace std

Todos os arquivos na biblioteca padrão C++ declaram todas as suas entidades no namespace std. Por isso temos geralmente incluído o “using namespace std;” em todos os programas que usaram qualquer entidade definida em iostream.


Anúncios
%d blogueiros gostam disto: