Templates
Traduzido de: http://www.cs.washington.edu/people/acm/tutorials
Prepared for Bart Niswonger’s CSE 326 (Data Structures) class, Su ’01
– Albert Wong (awong@cs)
Razões de uso de templates
O que são templates? Vale a pena usa-los?
• Programação Genérica Limitada (polimorfismo)
Algumas funções tem o mesmo significado semântico para alguns (ou
talvez todos) tipos de dados. A função print() deve exibir uma
representação de qualquer coisa passada a ela. Não deveria haver
necessidade de reescrever esta função para cada possível tipo de dado.
• Código menos repetitivo
Código que difere apenas no tipo de dado que manipula não deve ser
reescrito para cada tipo de dados que se pretende manipular. É mais fácil
de ler e manter se um mesmo trecho de código for usado para todos os
tipos de dados.
2
Exemplo:
uma função swap
Problema: Pode ser interessante trocar os valores de duas variáveis. O
comportamento desta função é semelhante para qualquer tipo de dado.
Funções template permitem esta troca, na maioria das vezes sem qualquer
mudança na sintaxe.
Método estúpido – escrever uma função sobrecarregada para cada tipo
Swap 4
void swap(int &a, int &b) {
int c = a;
a = b;
b = c;
}
Swap 4 T
void swap(T &a, T &b) {
T c = a;
a = b;
b = c;
}
Método template – escrever uma função template
template <typename T>
void swap(T &a, T &b) {
T c = a;
a = b;
b = c;
}
Esta função pode ser usada com
qualquer tipo de dado que suporte
atribuição e pode ser passada por
referência não constante.
3
Sintaxe de Template:
A linha <…> estabelece que tudo da
declaração ou definição que se
segue pertence ao template (no caso
a definição da função swap)
template <typename T>
void swap(T &a, T &b) {
T c = a;
a = b;
b = c;
}
As “variáveis placeholder” tem um valor para
cada declaração de template. Elas serão
substituídas por qualquer tipo que seja
especificado para o template
swap dissecado
Lista de “variáveis placeholder”. Na
maioria das vezes elas serão
especificadas pelas palavras chave
typename ou class. Estas palavras
chave são eqüivalentes
Comportamento de Template:
Como muitas coisas em C++ os
templates comportam-se como parte
da linguagem. Mas não são. Existem
duas maneira de empregar templates,
por especialização implícita ou
explícita. A especialização explícita
funciona sempre. A implícita algumas
vezes mas é muito mais “limpa”
4
Sintaxe de Template :
Usando um template
Para usar um template há necessidade de sua
especialização. Templates não são funções genéricas.
Eles fazem polimorfismo estático. Eles se
metamorfoseiam no tipo correto em tempo de préprocessamento
Uso
template <typename T>
void swap(T &a, T &b) {
T c = a;
a = b;
b = c;
}
Sintaxe
Na especialização explícita de um template escreve-se seu nome com os argumentos para as variáveis
placeholder entre parênteses angulosos. Isto sempre funciona.
Exemplo:
double d1 = 4.5, d2 = 6.7;
swap<double>(d1, d2);
Templates podem detectar os valores de seus placeholders se toda a informação sobre o que o
placeholder representa possa ser inferida do contexto(argumentos e, para funções membro, a instância
associada). Isto é chamado de especialização implícita. No caso acima o compilador detecta que T é
um double mesmo sem o <double> explícito uma vez que os argumentos são double. Esta
simplificação funciona.
Exemplo:
swap(d1, d2);
5
Como funciona
Libraries
Pré-processador
Resolve #define, #include,
comments, templates
Compilador
Traduz o código para linguagem
de máquina. Produz um “object
file.” Este código não é
executável
Linker
Código fonte
(text)
.c .h
.c .h
Pré-processador
Código C/C++
(text)
Código objeto
(bin)
Executável Nativo
(bin)
Compiler
Linker
executável
Recebe os object files e resolve
as referências para as funções e
variáveis em arquivos distintos
Compiladores possuem no mínimo 2 partes e C++ tem 3 partes. Quando se
constrói um executável a partir de um fonte C++, o pré-processador remove
tudo listado sob “Preprocessor.” O resultado é código puro C++ (sem
commentários, templates, #includes, etc). Este código é então compilado e
ligado.
6
Como funciona:
Compilador, Linker
Compilação
• Compilar significa transformar uma linguagem em outra linguagem. Em C++
transforma-se fonte C++, em código de máquina
• Cada arquivo fonte C++ usualmente é compilado em um object file que contém o
código e todas as funções definidas. Caso seja invocada uma função de uma
biblioteca ou outro arquivo, o código objeto (no object file) afirma “esta função
existe em algum lugar e quero usá-la”
• Os erros de sintaxe usualmente são mensagens do compilador (não do linker nem
do pré-processador).
Edição de ligações
• Depois da compilação, todos os arquivos objeto precisam ser ligados para criar um
executável final. Todos os “stubs” “quero esta função” nos arquivos objeto tem de ser
resolvidos em um bloco de código de máquina e o executável resultante tem de ser
formatado de maneira inteligível pelo sistema operacional.
• Se for escrito o protótipo de uma função, sem sua definição, o código compilará mas
não ligará. Os erros de link usualmente são mais difíceis de rastrear, pois linker não
pode sempre fornecer os números das linhas (o linker vê apenas os arquivos objeto e
não tem acesso ao código fonte.
7
Como funciona:
Pré-processador
O que é o pré-processador?
O pré-processador trata com as diretivas que iniciam com # (tais como #include,
#define). O pré-processamento trata de tudo que ocorre antes do compilador começar a
transformação para código de máquina.
Em relação aos templates as coisas relevantes que o pré-processador faz são:
• Substitui todos os #include pelos arquivos que eles referenciam.
• Remove todos os comentários.
• Substitui todas as macros #defines por seus valores
• Gera o código dos templates
Templates não existem!
• Templates são construções do pré-processador. Quando um template é usado
(especializado, implicitamente or explicitamente), ele se torna instanciado.
• Para criar a instância o pré-processador cria versão do template na qual cada
placeholder é substituído por sua especialização. Neste ponto a versão específica do
template passa a existir e pode ser compilada. Ela não existe de outra maneira!
• Um template apenas faz uma busca e substituição para cada tipo especializado para o
template.
8
Como funciona:
Conseqüências
Problema:
Templates são resolvidos na etapa de pré-processamento, não existindo
para o compilador até serem instanciados. Procura-se ser transparente e
eficiente. Como esperado, C++ falha e às vezes as coisas se tornam
inconsistentes.
Efeitos:
• O código do template não é compilado até ser usado (e instanciado). O
compilador não detecta erros de sintaxe até o template ser usado.
• A especialização (momento de uso do template) instancia todos os
templates relevantes que o precedem. Se um template aparece depois
da especialização, ele não é instanciado nem compilado.
Conclusão:
Para que tudo funcione, todas as definições dos templates relevantes
devem aparecer antes de, pelo menos, uma especialização . Caso
contrário, partes do template não serão instanciadas e compiladas.
9
Class Templates:
Definição de Classe
Sintaxe:
A sintaxe para as templated classes acompanha a sintaxe das templated
functions. As regras para que as templated classes possam inferir sua
especialização são mais evoluídas.
Recordação:
Estes dois templates são equivalentes?
template <typename T>
void swap(T &a, T &b) {
T c = a;
a = b;
b = c;
}
template <class C>
void swap(C &a, C &b) {
C c = a;
a = b;
b = c;
}
Resposta:
Sim, são equivalentes. Escrevendo class templates pode-se chegar a uma
situação na qual duas definições sejam escritas para a mesma coisa. Se isto
ocorrer o programa não será montado (build). O nome do placeholder não
interessa, e “typename” e “class” podem ser usados de maneira
intercambiável.
10
Class Templates:
Exemplo
Exemplo: Um array templated, dinâmico, 2 dimensional (Matrix)*
A única coisa que muda na
definição da classe é a linha:
template <typename T>
No bloco de
definição o
placeholder (T)
pode ser usado
como um tipo de
dados. Quando o
template é
especializado, ele
assume o valor da
especialização.
#ifndef MATRIX_H
#define MATRIX_H
template <typename T>
class Matrix {
public:
Matrix(int rows, int cols);
Matrix(const Matrix &other);
virtual ~Matrix();
Matrix& operator=(const Matrix &rhs);
T* operator[](int i);
int getRows() const;
int getCols() const;
protected:
void copy(const Matrix &other);
private:
Matrix();
int m_rows;
int m_cols;
T *m_linArray;
Arquivo: Matrix.h
};
#endif /* MATRIX_H */
11
Class Templates:
Continuação do exemplo
#include "Matrix.h"
template <typename T>
Matrix<T>::Matrix()
{}
template <typename T>
Matrix<T>::Matrix(int rows, int cols) {
m_rows = rows;
m_cols = cols;
m_linArray = new T[m_rows *
m_cols];
}
template <typename T>
Matrix<T>::Matrix(const Matrix &other) {
copy(other);
}
template <typename T>
Matrix<T>::~Matrix() {
delete[] m_linArray;
}
template <typename T>
T* Matrix<T>::operator[](int i) {
return m_linArray + (i*m_cols);
}
template <typename T>
void
Matrix<T>::copy(const Matrix &other) {
m_rows = other.m_rows;
m_cols = other.m_cols;
int size = m_rows * m_cols;
m_linArray = new T[size];
for( int i=0; i < size; i++ ) {
m_linArray[i] =
other.m_linArray[i];
}
}
template <typename T>
int Matrix<T>::getRows() const {
return m_rows;
}
template <typename T>
template <typename T>
int Matrix<T>::getCols() const {
Matrix<T>&
Matrix<T>::operator=(const Matrix &other) { return m_cols;
}
if( this != &other ) {
delete[] m_linArray;
copy(other);
}
return *this;
}
Arquivo: Matrix.cpp
12
Class Templates:
O nome de uma templated class, por si só,
não significa nada (Matrix não tem nenhum
significado). O significado só é adquirido
pela especialização (explícita ou implícita).
Quando for referenciada uma instância
(uma especialização específica) de uma
templated class, o nome da classe deve ser
especializado explícitamente.
Observe que a
região de
especialização
não inclui o tipo
de retorno.
Portanto, o tipo
de retorno
necessita de
especialização
explícita.
Funções Membro Dissecadas
O template foi especializado
implicitamente pelo seu contexto. Está
dentro da região de especialização do
escopo da classe. Assim não precisa
dos argumentos do template. Para
uma definição de classe a região de
especialização é o bloco da classe .
template <typename T>
Matrix<T>&
Matrix<T>::operator=(const Matrix &other) {
if( this != &other ) {
this->~Matrix();
copy(other);
}
return *this;
}
specialization region of
Matrix<T>::
Lembrar que
embora construtores
e destrutores
tenham o mesmo
nome do template
de classe eles são
funções e não
necessitam
especialização.
13
Class Templates:
Sintaxe
•As templated classes precisam ser especializadas explicitamente.
Para criar uma 2 dimensional Matrix de doubles do último exemplo,
a sintaxe seria:
Matrix<double> m(3,3);
•Esta especialização durante a declaração cria um novo tipo –
Matrix<double>. É um tipo próprio, separado de qualquer outra
especialização de Matrix (diferente de Matrix<int>, ou
Matrix<Foo>, etc.) Neste ponto a instância se comporta como
qualquer outro tipo instanciado – pelo menos para efeito de
compilação.
14
Segurança:
Consciência do perigo
Problema
Templates não existem até serem usados. Necessitam ser instanciados, Se
a instanciação não ocorrer de maneira explicita, ela ocorre no primeiro uso
de todas as definições do template. Considere-se o exemplo
Parece bem mas não ligará.
O que não funciona e por que?
O link error ocorre com m1.getRows()
• Nada de um template é instanciado até
que seja usado ou instanciado
explicitamente.
• Matrix<int>::getRows() const não é
criada até que seja usada na linha
contendo m1.getRows().
• A definição da função está em Matrix.cpp
e nunca foi usada lá.
Portanto a definição nunca é criada e
compilada para código objeto.
/* main.cpp */
#include <iostream>
using namespace std;
#include “Matrix.h”
int main(void) {
Matrix<int> m1(3,4);
cout << m1.getRows() << endl;
}
O arquivo Matrix.cpp só contém código de
template. Como nunca é usado, nunca gera
código objeto e não pode ser compilado.
15
Segurança:
3 convenções
Existem três convenções para evitar o problema de ligações
• Escreva todo o código inline nos arquivos .h.
• Faça o dito anteriormente mas escreva um arquivo de implementação com a sua
implementação e #include o arquivo de implementação em seu arquivo header.
• Escreva o template como uma classe normal (usando arquivos header e de
implementação). A seguir crie um novo Este é o arquivo a ser compilado e não a
implementação do template arquivo fonte e #include o arquivo de implementação do
template nele.
Os dois primeiros métodos sofrem do problema de, sempre que a implementação de
uma função seja mudada, todo o código que a usa deve ser recompilado (e não
apenas re-ligado). Isto é lento em montagens grandes. O processo de construção irá
instanciar o template muito maior número de vezes do que o necessário (com perda de
tempo e espaço).
O terceiro método fica livre destes problemas também evitando outras barreiras uma
vez que força a instanciação de tudo em um só ponto.
16
Segurança :
/* main.cpp */
#include <iostream>
using namespace std;
#include <Matrix.h>
int main(void) {
Matrix<int> m1(3,4);
cout << m1.getRows() << endl;
}
Exemplo:
Para conseguir ligar de maneira
apropriada o código anterior é
necessário instanciar uma versão int
do template de Matrix. O arquivo
fica com o aspecto indicado
Um exemplo
Procedimento adequado
• Escreva o template, de maneira
separada entre um header e um
arquivo de implementação
• Crie um arquivo de instanciação para
o template que inclua o arquivo de
implementação.
• Compile o arquivo de instanciação e
não o arquivo de implementação do
template
• O arquivo de instanciação gera o
código objeto para o template.
/* MatrixInst.cc */
#include “Matrix.cpp”
template Matrix<int>;
Observe que o
arquivo de
implementação
(não o header) é
incluído.
Esta linha força a instanciação do template da classe
Matrix, bem como de todas as suas funções membro,
para a especialização int. Outras especializações
17
exigem suas próprias linhas.
Detecção de falhas
(fique ligado para descobrir antes que seja tarde demais)
/*Foo.h */
#ifndef FOO_H
#define FOO_H
template <typename T>
class Foo {
Foo() {
b = “Hello Mom!”;
}
};
#endif /* FOO_H */
Este código compilará?
• Infelizmente sim. Apesar de b não ser
declarado, vai “compilar” porque
ninguém instancia o template, e o
compilador nunca vê o código do
template.
• Esta é razão pela qual não forçar
explicitamente a instanciação dos
class templates é perigoso. Não se
percebe o erro até tentar usar o
código.
Por esta razão algumas pessoas julgam melhor escrever uma primeira versão
da classe sem usar templates, e criar templates a partir dai.
18
STL
Standard Template Library, ou STL, é uma
biclioteca C++ de classes de containers,
algoritmos e iteradores
Fornece muitos algoritmos básicos e estruturas de
dados
STL é uma biblioteca genérica com componentes
are altamente parametrizados: quase todos os
componentes da STL são templates. Não se deve
usar a STL enquanto não se conheça o
funcionamento dos templates em C++
19
Exemplo de uso de STL
/* **********************************
* Stack.hpp
* ********************************/
#ifndef STACK_HPP
#define STACK_HPP
// is stack empty?
bool empty() const {
return c.empty();
}
// push element into the stack
void push (const T& elem) {
c.push_back(elem);
}
#include <deque>
#include <exception>
template <class T>
class Stack {
protected:
std::deque<T> c;
// pop element out of the stack and
// return its value
T pop () {
if (c.empty()) {
throw ReadEmptyStack();
}
T elem(c.back());
c.pop_back();
return elem;
}
//container for the elements
public:
/* exception class for pop() and top() with empty
stack /*
class ReadEmptyStack : public std::exception
{
public:
virtual const char* what() const throw() {
return "read empty stack";
}
};
{
// number of elements
typename std::deque<T>::size_type size() const
}
return c.size();
};
// return value of next element
T& top () {
if (c.empty()) {
throw ReadEmptyStack();
}
return c.back();
}
#endif /* STACK_HPP
20
Download

Templates não existem!