Introdução à STL e templates em C++

Introdução
Templates em C++
Introdução à STL: vector e map
Iterators
Algoritmos
Como compilar, e links úteis

Introdução

Em vários programas precisamos de fazer tarefas comuns: criar colecções de dados, adicionar elementos a essas colecções, remover, alterar, procurar por elementos, ordenar uma sequência qualquer, etc. A STL (Standard Template Library) é uma biblioteca de templates que faz parte do standard C++ que contêm implementações de vários tipos de colecções mais comuns e de vários algoritmos. Nesta página apresenta-se como usar templates em C++ e como usar a STL. Pressupõe-se conhecimentos básicos de C ou C++ e algumas noções de estruturas de dados.

Templates em C++

Para perceber para que servem os templates, vamos analisar um caso simples: queremos criar uma biblioteca para criar e manipular stacks de inteiros. Em C++, podíamos fazer qualquer coisa do género:

class Stack {
	public:
		Stack(int size) {
			stack = new int[size];
			msize = size;
			ptr = 0;
		}
		
		~Stack() {
			delete[] stack;
		}

		int size() {
			return ptr;
		}

		void push(int i) {
			if (ptr < msize)
				stack[ptr++] = i;
			else
				// whatever
		}

		int top() {
			if (ptr > 0)
				return stack[ptr-1];
			else
				// whatever
		}

		void pop() {
			if (ptr > 0)
				ptr--;
			else
				// hm, whatever
		}

	private:
		int ptr;
		int msize;
		int *stack;
};
Há vários aspectos desta implementação que são discutíveis, mas o que importa aqui é a questão simples: e se eu precisar de um stack não de inteiros, mas de doubles?

Claro que posso copiar o código todo e alterar int para double. E se quiser os 2 ao mesmo tempo? E se depois quiser um stack de objectos de uma classe qualquer que ainda nem sei qual é? Já se vê o problema: esta implementação não é genérica, ou seja, não pode ser usada com qualquer tipo de dados. Os Templates resolvem este problema. Antes de explicar, aqui está o código do mesmo stack mas agora genérico, ou seja, um stack que pode ser de um tipo qualquer:

template<class Type>
class Stack {
	public:
		Stack(int size) {
			stack = new Type[size];
			msize = size;
			ptr = 0;
		}
		
		~Stack() {
			delete[] stack;
		}

		int size() {
			return ptr;
		}

		void push(const Type &t) {
			if (ptr < msize)
				stack[ptr++] = t;
			else
				// whatever
		}

		Type top() {
			if (ptr > 0)
				return stack[ptr-1];
			else
				// whatever
		}

		void pop() {
			if (ptr > 0)
				ptr--;
			else
				// hm, whatever
		}

	private:
		int ptr;
		int msize;
		Type *stack;
};
Este código ajuda imenso a ver como se usam os templates: usa-se a keyword template, seguida do nome genérico que vamos usar entre < e >. Neste caso, o nome é Type. A linha completa fica template<class Type>. Se quisermos usar 2 tipos genéricos, fica template<class Foo, class Bar> (podem ser quantos forem precisos). Como o nome que usámos foi Type esse é também o nome que usamos para nos referirmos a esse tipo no código que se segue. Neste caso, o código abrangido pela definição do template é a definição da classe Stack. Se o código das funções fosse definido fora da definição da classe, teria de ser assim:

template
Type Stack::top()
{
	if (ptr > 0)
		return stack[ptr-1];
	else
		// whatever
}
Pronto, esta parte foi simples. Agora como se cria um stack por exemplo, de inteiros? E de doubles?
int main()
{
	Stack<int> pilha(10);
	Stack<double> pilhaReal(10); // ai
	
	pilha.push(2);
	pilhaReal.push(3.14);

	cout << pilha.top() << "\n";

	pilhaReal.pop();

	return 0;
}
Portanto, se quiserem usar um stack de objectos de uma classe chamada MuitaClasse, o código é Stack<MuitaClasse> o_meu_stack(100).

A STL inclui várias classes que implementam várias estruturas de dados como stacks, queues, vectors, maps, etc e algoritmos para operar sobre elas. Como o nome indica, as classes da STL são definidas com templates para poderem conter qualquer tipo de dados.

Introdução à STL: vector e map

Vamos começar pelo vector, uma classe muito útil e simples. Um vector é como um array normal, mas cresce dinamicamente para conter mais elementos. Um exemplo de utilização:
#include <vector>

using std::vector;

int main()
{
	vector<int> v;

	v.push_back(4);
	v.push_back(2);
	v.push_back(3);
	v[0] = 1;

	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << endl;
	}
	
	return 0;
}
O output seria:
1
2
3

O vector inclui outros métodos úteis que podem consultar na referência. Outro container útil é o map. O map associa uma chave a um item. Para definir um objecto do tipo map, é preciso especificar 2 tipos, o da chave e o dos items. Por exemplo, um vector<int> pode ser considerado um map com chaves do tipo int e items do tipo int também. Um vector<string> é como um map com chaves tipo int e items tipo string. Uma diferença crucial é que no map podemos ter como único elemento, por exemplo, mapa[666] = "buedabad"; enquanto que no vector, para isso temos de ter todos os elementos até 666. Bem, algum código para ver como funciona:

#include <map>
#include <string>
#include <iostream>

using namespace std;

int main()
{
	map<string, int> digitos;
	string s;

	digitos["zero"] = 0;
	digitos["um"] = 1;
	digitos["dois"] = 2;
	digitos["três"] = 3;
	digitos["quatro"] = 4;
	digitos["cinco"] = 5;
	digitos["seis"] = 6;
	digitos["sete"] = 7;
	digitos["oito"] = 8;
	digitos["nove"] = 9;

	while (cin >> s) {
		cout << digitos[s] << endl;
	}
	
	return 0;
}
Se a entrada for:
seis
nove

A saída será:
6
9

Acho que dá para perceber como funcionam os maps :) Para poder introduzir outras funcionalidades da STL, temos de falar primeiros de iterators

Iterators

O que são iterators? Por exemplo, neste ciclo:
for (i = 0; i < size; i++) {
	frobnicate(array[i]);
}
A variável i funciona aqui como um iterator, ou seja, ela está a iterar sobre o array. Mas por exemplo, num mapa como o que usámos no exemplo anterior, como poderíamos iterar sobre os elementos do mapa? A solução da STL é usar iterators. Iterators são iteradores genéricos, que fazem basicamente o mesmo trabalho que o i neste ciclo, mas que podem ser usados com estruturas mais exóticas. Um exemplo:

#include <map>
#include <string>
#include <iostream>

using namespace std;

int main()
{
	map<string, int> digitos;
	map<string, int>::iterator iter; // o nosso iterator
	string s;

	digitos["zero"] = 0;
	digitos["um"] = 1;
	digitos["dois"] = 2;
	digitos["três"] = 3;
	digitos["quatro"] = 4;
	digitos["cinco"] = 5;
	digitos["seis"] = 6;
	digitos["sete"] = 7;
	digitos["oito"] = 8;
	digitos["nove"] = 9;
	
	for (iter = digitos.begin(); iter != digitos.end(); iter++) {
		cout << iter->first << " = " << iter->second << endl;
	}

	return 0;
}
O output seria:
cinco = 5
dois = 2
nove = 9
oito = 8
quatro = 4
seis = 6
sete = 7
três = 3
um = 1
zero = 0

A ordem não é aleatória: estão ordenados por ordem alfabética, ou seja, por ordem crescente das chaves. Para definir um iterator sobre um map<string, int> fazemos map<string, int>::iterator i;. Este iterator só pode ser usado sobre objectos do tipo map<string, int>; não pode ser usado para iterar sobre, por exemplo, um vector<int> nem sobre um map<int, int>.

O método digitos.begin() retorna um iterator para o início do mapa; o digitos.end() retorna um iterator para um elemento após o último do mapa. O iterator devolvido pelo digitos.end() nunca é válido, serve só para saber quando chegámos ao fim ou para saber se tentámos aceder a um elemento não existente; por exemplo, para verificar se uma dada string existe num mapa:

if (digitos.find("onze") == digitos.end()) {
	// não existe!
} else {
	// existe!
}

Algoritmos

A STL também define vários algoritmos. Por exemplo, para ordenar os elementos de um vector usamos o sort:
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

int main()
{
	int n;
	vector<int> v;
	
	cout << "0 para parar:\n";
	while (cin >> n) {
		if (n == 0)
			break;
		v.push_back(n);
	}

	sort(v.begin(), v.end());

	for (vector<int>::iterator iter = v.begin(); iter != v.end(); iter++) {
		cout << *iter << endl;
	}

	return 0;
}
O sort precisa de 2 argumentos, um iterator para o início da sequência a ordenar e outro para o fim. Aqui se vê mais uma vez a utilidade dos iterators: se em vez de um vector tivéssemos usado uma lista, o sort seria chamado da mesma forma, e ordenava a lista na mesma sem saber se era uma lista ou um vector, porque só actua sobre os iterators.

Como compilar, e links úteis

Para compilar estes programas não é preciso fazer nada em especial, não é preciso linkar com nenhuma biblioteca especial. Com o gcc: g++ main.cpp -o main.

Isto foi só uma introdução para poder usar o vector e o map mas dá para fazer mais coisas com eles. Também existem classes úteis como a queue, set, list, e outras. Para saber o que há para saber sobre a STL o melhor sítio a consultar é a referência da SGI. Podem começar aqui.

Em caso de haver alguma incorrecção por aqui ou precisarem de algum esclarecimento, mandem um mail: a28123@alunos.det.ua.pt