Templates e STL
Código genérico, escolha de contentores, iteradores, invalidação, algoritmos e comparadores.
maior(3, 8) compara inteiros; maior(2.5, 1.5) compara reais. Um parâmetro de tipo permite escrever o mesmo corpo para ambos. A biblioteca padrão usa esta ideia para combinar contentores que guardam valores, iteradores que delimitam intervalos e algoritmos que trabalham nesses intervalos.
Funções template
template <typename T> introduz um parâmetro de tipo. typename e class são equivalentes nesta declaração simples. Na chamada maior(3, 8), o compilador deduz T = int; em maior(2.5, 1.5), deduz double.
O exemplo combina uma função e uma classe template; a pilha guarda os dados num vector, onde push_back acrescenta no fim, back consulta o último, pop_back remove-o e empty testa o vazio. A retirada verifica o vazio antes de usar back.
Programa completo para compilar localmente:
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
template <typename T>
T maior(T a, T b) { return a < b ? b : a; }
template <typename T>
class Pilha {
public:
void inserir(const T& valor) { dados_.push_back(valor); }
T retirar() {
if (dados_.empty()) throw std::out_of_range("Pilha vazia");
T valor = dados_.back();
dados_.pop_back();
return valor;
}
bool vazia() const { return dados_.empty(); }
private:
std::vector<T> dados_;
};
int main() {
std::cout << maior(3, 8) << " " << maior(2.5, 1.5) << "\n";
Pilha<std::string> palavras;
palavras.inserir("primeira");
palavras.inserir("segunda");
std::cout << palavras.retirar() << " " << palavras.retirar() << "\n";
std::cout << std::boolalpha << palavras.vazia() << "\n";
}
Saída:
8 2.5
segunda primeira
true
O corpo de maior exige que a < b seja válido. maior(3, 2.5) não deduz um único T, porque os argumentos têm tipos diferentes; escreve maior<double>(3, 2.5) para escolher double.
Um template não elimina requisitos dos tipos. maior(objeto1, objeto2) só funciona se esse tipo oferecer a comparação necessária. As definições dos templates têm de estar disponíveis quando o compilador as instancia, por isso ficam habitualmente no cabeçalho, ao contrário de funções não template cuja implementação pode ficar num .cpp separado.
Classes template
Pilha<std::string> e Pilha<int> são tipos distintos. Cada instância conserva a mesma disciplina, último a entrar, primeiro a sair, mas guarda valores de outro tipo. A retirada do exemplo copia o último valor antes de o remover e lança uma exceção se a pilha está vazia. Exige, portanto, um tipo copiável. Um tipo só com movimento precisaria de outra retirada, que devolvesse por movimento em vez de copiar.
Depois das inserções, a pilha contém primeira, segunda; a primeira retirada devolve segunda e deixa primeira, a seguinte devolve primeira e deixa o vetor vazio. Uma terceira retirada lançaria uma exceção, em vez de consultar um elemento inexistente.
vector: o array que cresce
std::vector<T> tem armazenamento contíguo, tamanho variável e acesso por
índice. size() conta os elementos existentes; capacity() conta a capacidade em elementos. reserve(n) só aumenta a reserva quando n excede a capacidade atual; nunca a diminui nem cria elementos. resize(n) muda o tamanho, construindo ou destruindo elementos.
v[i] exige i < v.size() e não verifica o limite. v.at(i) verifica e lança std::out_of_range. front, back e pop_back exigem um vetor não vazio.
Reservar capacidade para dez elementos não permite escrever v[9] se o tamanho ainda é zero. Depois de reserve(3), o tamanho continua 0 e a capacidade passa a pelo menos 3. Depois de resize(3), existem os índices 0, 1 e 2.
push_back tem custo amortizado constante: algumas inserções são mais caras porque realocam e transferem os elementos. Inserir ou remover no meio desloca elementos e custa linear no número deslocado. A gestão automática de memória não elimina os custos das operações.
Escolher um contentor
Nesta tabela, n é o número de elementos. Os custos usuais de procura por chave pressupõem comparações ou dispersão de custo constante.
| Contentor | Organização | Acesso e operação úteis |
|---|---|---|
array<T, N> | Contíguo, tamanho fixo | Índice em tempo constante |
vector<T> | Contíguo, tamanho variável | Índice constante; adicionar no fim amortizado constante |
deque<T> | Sequência de blocos | Índice constante; inserir nos extremos constante |
list<T> | Nós ligados | Inserir/remover com iterador conhecido constante; procura linear |
set<T> | Chaves únicas ordenadas | Procura, inserção e remoção logarítmicas |
map<K, V> | Chaves únicas ordenadas com valores | Procura por chave logarítmica |
unordered_map<K, V> | Tabela de dispersão | Procura média constante, pior caso linear |
Uma lista não oferece acesso por índice. Encontrar o nó pode custar linear, mesmo que a remoção desse nó seja constante. stack e queue são adaptadores que expõem as operações de pilha e fila sobre um contentor subjacente. Escolhe pela sequência de operações de que precisas, não só por uma complexidade isolada.
map e set: chaves e conjuntos
#include <iostream>
#include <map>
#include <sstream>
#include <string>
int main() {
std::istringstream entrada("azul verde azul vermelho verde azul");
std::map<std::string, int> frequencias;
std::string palavra;
while (entrada >> palavra) ++frequencias[palavra];
for (const auto& par : frequencias)
std::cout << par.first << ": " << par.second << "\n";
std::cout << std::boolalpha << (frequencias.find("branco") == frequencias.end())
<< " " << frequencias.size() << "\n";
}Executa o código para ver a saída e os gráficos.
Saída:
azul: 3
verde: 2
vermelho: 1
true 3
frequencias[palavra] insere a chave a zero se ela ainda não existe; serve para contar, mas surpreende numa simples consulta, por isso usa find quando queres consultar sem inserir. Cada elemento de um map é um pair<const Key, Valor>: par.first é a chave e par.second é a contagem.
map percorre as chaves pela ordem do comparador. set guarda apenas as chaves, sem o segundo valor, e ignora inserções repetidas de uma chave equivalente. unordered_map não oferece ordem de percurso fixa.
Iteradores
Um iterador identifica uma posição num contentor. begin() identifica o primeiro elemento e end() a posição depois do último. No vazio, são iguais. Um intervalo [first, last) pode representar o vazio sem caso especial; percorre enquanto it != last, lê *it e avança com ++it. Nunca desreferencies end().
// Excerto: v é um vector<int> já existente.
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
Iteradores têm capacidades diferentes. vector fornece acesso aleatório e permite it + 3. list fornece avanço e recuo, mas não soma de um deslocamento. std::sort exige acesso aleatório; numa list, usa a sua operação sort().
Invalidação de iteradores
Um iterador ou referência pode deixar de ser válido quando a coleção muda. Num vector, uma realocação invalida todos os iteradores, apontadores e referências aos elementos. Se uma inserção não realoca, os acessos anteriores à posição conservam-se, mas os da posição em diante, incluindo o antigo end, são invalidados. erase invalida os acessos na posição removida e posteriores.
Não guardes um iterador e continues a usá-lo depois de uma operação que o pode invalidar. Para remover durante um percurso de vector, a forma habitual é it = v.erase(it) quando removes, e ++it quando conservas. erase devolve um novo iterador válido para o elemento seguinte, ou end().
Em list e nos contentores associativos ordenados, remover um elemento invalida os acessos a esse elemento, e inserir normalmente não invalida os anteriores. As regras de deque e dos contentores de dispersão diferem; não generalizes a regra do vetor.
Algoritmos: sort e find
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> valores = {5, 2, 3, 2, 1, 4, 5};
std::sort(valores.begin(), valores.end());
valores.erase(std::unique(valores.begin(), valores.end()), valores.end());
valores.erase(std::remove_if(valores.begin(), valores.end(),
[](int x) { return x % 2 == 0; }), valores.end());
for (int x : valores) std::cout << x << " ";
std::cout << "\n" << std::accumulate(valores.begin(), valores.end(), 0) << "\n";
}Executa o código para ver a saída e os gráficos.
Saída:
1 3 5
9
Depois de ordenar, unique compacta valores iguais adjacentes e devolve o novo fim lógico sem mudar o tamanho. No exemplo, o tamanho continua 7 até ao erase. O resultado lógico termina no iterador devolvido. As posições depois dele continuam desreferenciáveis, com estado válido mas não especificado, por isso o programa não conta com esses valores.
O primeiro erase apaga esse resto e reduz o tamanho para 5. O segundo remove os pares filtrados por remove_if.
find percorre até encontrar um valor ou chegar ao fim. count_if conta os que satisfazem um predicado. accumulate, de <numeric>, começa no valor inicial recebido; esse valor também influencia o tipo do acumulador. Para somar double, usa 0.0 quando queres acumulação real.
Uma lambda, como [](int x) { return x % 2 == 0; }, cria uma operação sem lhe dar um nome separado. Lê [] como a lista de capturas: vazia aqui, [limite] copia uma variável e [&limite] referencia-a. Uma captura por referência não prolonga a vida do objeto capturado. No programa de filtragem acima, a lambda decide que elementos são pares; remove_if desloca os restantes para o início.
Comparadores e pesquisa ordenada
Um comparador de ordenação deve representar “vem antes”, com uma relação estrita consistente. Para qualquer x, comp(x, x) tem de ser falso. Por isso a <= b não serve como comparador de sort: dá verdadeiro para valores iguais e quebra a exigência. Para desempatar objetos, compara primeiro o critério principal e depois o secundário:
// Excerto: ordena por nota e desempata por identificador.
struct Registo { int nota; int id; };
bool antes(const Registo& a, const Registo& b) {
if (a.nota != b.nota) return a.nota < b.nota;
return a.id < b.id;
}
Com {10:2, 10:1, 9:3}, a ordem é 9:3, 10:1, 10:2. Verificar apenas comp(x, x) == false não prova transitividade; prova só o caso mais simples. Um comparador que ordena por média e desempata por nome usa a mesma estrutura: primeiro o valor, depois a chave estável.
Para pesquisar, lower_bound encontra a primeira posição que não vem antes do valor procurado. O intervalo tem de estar particionado por esse critério, normalmente ordenado pelo mesmo comparador. Num vetor {1, 3, 5, 7}, procurar 5 devolve a posição 2; procurar 4 devolve a mesma posição, que contém 5. Encontrar uma posição de inserção não prova que o valor existe: confirma it != v.end() antes de ler *it e compara o valor encontrado com o procurado.
sort não garante a ordem anterior entre elementos equivalentes; stable_sort conserva-a. Procurar por pesquisa binária num vetor arbitrário não satisfaz a condição de partição.
Agregar por chave e ordenar pelo resultado
Um map<string, list<int>> liga cada estudante às suas notas e percorre os nomes pela ordem das chaves. Se o relatório pede ordem decrescente de média, esse percurso não chega. Calculamos um resumo por estudante e ordenamos os resumos pelo critério pedido, conservando o mapa original.
Cada lista contém entre 1 e 1000 notas inteiras de 0 a 20. O resultado usa a média de cada estudante e, no fim, a média das médias, dando o mesmo peso a cada estudante. Para o mapa vazio, escrevemos Sem estudantes. Estes contratos evitam dividir pelo tamanho de uma lista vazia ou pelo número zero de estudantes.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <list>
#include <map>
#include <string>
#include <vector>
struct Media {
std::string nome;
double valor;
};
// Cada lista tem 1 a 1000 notas inteiras de 0 a 20.
void imprimir_medias(const std::map<std::string, std::list<int>>& notas) {
std::vector<Media> resumo;
double soma_medias = 0;
for (const auto& estudante : notas) {
double soma = 0;
for (int nota : estudante.second) soma += nota;
double media = soma / estudante.second.size();
resumo.push_back({estudante.first, media});
soma_medias += media;
}
std::sort(resumo.begin(), resumo.end(), [](const Media& a, const Media& b) {
if (a.valor != b.valor) return a.valor > b.valor;
return a.nome < b.nome;
});
std::cout << std::fixed << std::setprecision(2);
for (const auto& estudante : resumo)
std::cout << estudante.nome << " " << estudante.valor << "\n";
if (resumo.empty()) std::cout << "Sem estudantes\n";
else std::cout << "Global " << soma_medias / resumo.size() << "\n";
}
int main() {
imprimir_medias({{"Ana", {10, 20}}, {"Bruno", {12}}, {"Carla", {13, 17}}});
}Executa o código para ver a saída e os gráficos.
Saída:
Ana 15.00
Carla 15.00
Bruno 12.00
Global 14.00
Ana tem (10 + 20) / 2 = 15, Bruno tem 12 / 1 = 12 e Carla tem (13 + 17) / 2 = 15. A soma começa em double, por isso a divisão não perde a parte fracionária. O vetor guarda os três pares nome/média, incluindo os dois com média 15. Um map<double, string> usaria a média como chave única e perderia um dos estudantes empatados.
O comparador põe a maior média primeiro e desempata por nome crescente. Comparar com > mantém a relação estrita; usar >= daria verdadeiro para um objeto comparado consigo próprio. O desempate por nome é uma escolha explícita deste exemplo, não uma consequência de sort nem uma regra atribuída à prova antiga.
No fim, (15 + 12 + 15) / 3 = 14. Juntar todas as notas daria 72 / 5 = 14,4: nesse cálculo, os estudantes com duas notas pesariam o dobro do que tem uma. O denominador e o peso de cada grupo vêm do contrato, não do contentor escolhido.
fixed com setprecision(2) formata a impressão com duas casas decimais; não arredonda as médias guardadas antes de ordenar ou calcular o total. Se duas médias diferentes aparecerem iguais no ecrã, a ordem continua a usar os valores calculados. Para m estudantes e g notas no total, a agregação percorre as g notas e a ordenação faz O(m log m) comparações; o vetor adicional guarda m resumos.
Exercícios
Repetidos adjacentes e fim lógico
O vetor começa com 1, 1, 2, 1, 1, 4. Executa-se apenas
auto fim = std::unique(v.begin(), v.end());. Indica o tamanho de
v e o conteúdo de [v.begin(), fim). Repete o
raciocínio partindo dos mesmos dados, mas ordenando antes de
unique. Em cada versão, como se elimina a cauda física?
Pista
A primeira versão tem dois grupos de valores 1 separados pelo 2.
Solução
Sem ordenar, os grupos adjacentes são 1, 1, 2,
1, 1 e 4. O prefixo lógico fica
1, 2, 1, 4, com quatro elementos; o tamanho físico continua 6.
Depois de ordenar os dados originais, temos 1, 1, 1, 1, 2, 4.
unique deixa o prefixo 1, 2, 4, mas continua sem
reduzir o tamanho. Em ambas, v.erase(fim, v.end()) destrói a
cauda e ajusta o tamanho ao prefixo. Os valores da cauda antes de
erase são válidos, mas não especificados.
Confere a tua resposta:
- Obtive tamanho físico 6 e prefixo lógico 1, 2, 1, 4 na primeira versão.
- Obtive tamanho físico 6 e prefixo lógico 1, 2, 4 depois de ordenar.
- Escrevi erase para reduzir o tamanho e não atribuí valores fixos à cauda.
Um percurso para vector e list
Escreve uma função template que substitua por novo cada valor
igual a antigo no intervalo válido [inicio, fim) e
devolva o número de substituições. Deve servir tanto os iteradores de um
vector<int> como os de uma list<int>.
Os elementos admitem comparação e atribuição; antigo e
novo são recebidos por valor para não dependerem de elementos
que a função possa alterar.
template <typename It, typename T>
std::size_t substituir(It inicio, It fim, T antigo, T novo);Testa a sequência 2, 3, 3, 4, substituindo 3 por 9, e um
intervalo vazio. Justifica por que it + 1 limitaria o conjunto
de iteradores aceites. O enunciado adapta o problema de iteradores P11 do
suplemento FEUP_PROG.
Pista
Solução
// Inclui <cstddef> para std::size_t.
template <typename It, typename T>
std::size_t substituir(It inicio, It fim, T antigo, T novo) {
std::size_t total = 0;
for (auto it = inicio; it != fim; ++it) {
if (*it == antigo) {
*it = novo;
++total;
}
}
return total;
}Com 2, 3, 3, 4, as duas posições com 3 passam a 9, dando
2, 9, 9, 4 e resultado 2. No vazio, a condição é logo falsa e
o resultado é zero. Uma list permite ++it, mas
não it + 1. Não há inserção nem remoção, por isso este percurso
não invalida os iteradores de nenhuma das duas coleções.
Confere a tua resposta:
- Percorri [inicio, fim) com !=, * e ++, sem ler fim.
- Obtive 2 substituições e a sequência 2, 9, 9, 4 no caso dado.
- Tratei o intervalo vazio e não usei aritmética indisponível numa list.
Reconhecer um comparador inválido
Uma ordenação de notas usa
[](int a, int b) { return a <= b; }. Explica o
problema para notas iguais e corrige o critério. Indica também como
conservar a ordem anterior entre elementos com a mesma nota.
Pista
Testa o comparador com dois valores iguais.
Solução
Para duas notas iguais, a <= b dá verdadeiro. Isto viola a
exigência de ordenação estrita. Para ordem crescente, usa
a < b. Se queres conservar a ordem anterior entre notas
iguais, usa esse comparador com stable_sort.
Confere a tua resposta:
- Mostrei comp(x, x) verdadeiro no código inicial.
- Substituí <= por <.
- Expliquei quando stable_sort conserva a ordem anterior.
Erros frequentes
A ordem dos iguais e a validade do comparador são decisões diferentes.
Criar elementos, além de reservar espaço
O programa deve criar três elementos e escrever 0 1 2. Corrige
a preparação do vetor. Mantém at, para que um acesso inválido
continue a ser detetado.
#include <iostream>
#include <vector>
int main() {
std::vector<int> valores;
valores.reserve(3);
if (valores.size() != 3) {
std::cout << "Nao existem os tres elementos\n";
return 0;
}
for (std::size_t i = 0; i < 3; ++i) valores.at(i) = static_cast<int>(i);
for (int x : valores) std::cout << x << " ";
std::cout << "\n";
}Executa o código para ver a saída e os gráficos.
Pista
Consulta o tamanho depois de reserve, não só a capacidade.
Solução
Substitui valores.reserve(3) por
valores.resize(3), ou constrói logo
std::vector<int> valores(3). Agora existem os índices
0, 1 e 2, e o ciclo pode atribuir-lhes valores. A saída é
0 1 2.
Confere a tua resposta:
- Distingui size de capacity.
- Criei três elementos antes de lhes atribuir valores.
- Obtive a saída 0 1 2 sem erro de tamanho.
Erros frequentes
Trocar at por [] esconderia a verificação e introduziria comportamento indefinido, porque reserve não cria elementos.
Remover sem saltar o elemento seguinte
Define void remover_negativos(std::vector<int>& valores).
Altera o vetor recebido, conservando os restantes valores pela mesma ordem.
Nesta versão, usa um iterador e erase para seguir a
invalidação. Esta resposta é de autoavaliação.
#include <iostream>
#include <vector>
void remover_negativos(std::vector<int>& valores) {
// Escreve o percurso e a remoção.
}
void mostrar(std::vector<int> valores) {
remover_negativos(valores);
std::cout << "[";
for (int x : valores) std::cout << " " << x;
std::cout << " ]\n";
}
int main() {
mostrar({}); // [ ]
mostrar({-1, -2}); // [ ]
mostrar({1, -1, -2, 0, 3, 3, -4}); // [ 1 0 3 3 ]
}Executa o código para ver a saída e os gráficos.
Pista
Mais uma pista
Se removes, usa it = valores.erase(it). Se conservas, usa
++it. Os dois casos não devem fazer o mesmo avanço.
Solução
void remover_negativos(std::vector<int>& valores) {
auto it = valores.begin();
while (it != valores.end()) {
if (*it < 0) it = valores.erase(it);
else ++it;
}
}Em {-1, -2}, remover -1 devolve um iterador para -2.
Esse valor ainda precisa de ser analisado. Ao remover o último elemento,
erase devolve end() e o ciclo termina sem o ler.
Cada remoção pode deslocar o resto do vetor; no pior caso, esta versão tem
custo quadrático no número de elementos. A combinação
remove_if e erase da lição permite uma passagem
linear.
Confere a tua resposta:
- Removi todos os negativos, incluindo negativos consecutivos.
- Conservei zero, positivos, repetições e a ordem relativa.
- Usei o iterador devolvido por erase sem o avançar novamente.
Erros frequentes
Não uses um for com avanço incondicional depois de
erase. Não conserves o antigo end() nem uses um
iterador invalidado. Passar o vetor por valor alteraria apenas uma cópia.
Consultar sem inserir chaves
Compara duas cópias independentes do mesmo mapa, para a primeira consulta
não alterar a entrada da segunda. Um map<string, int> tem
azul: 3 e verde: 2. Queres saber se existe
branco sem alterar o mapa. Compara
frequencias[“branco”] com
frequencias.find(“branco”).
Pista
Solução
frequencias[“branco”] insere branco: 0 e o
tamanho passa a 3. find(“branco”) == end() responde que a
chave falta e o tamanho continua 2. Usa operator[] para
contar e find para consultar.
Confere a tua resposta:
- Distingui operator[] de find para consulta.
- Verifiquei que o tamanho não muda após a consulta.
- Expliquei quando cada um é adequado.
Ordenar sem perder o empate
Um map<string, list<int>> contém Duarte: {18, 12}, Beatriz: {15} e Caio: {9, 9, 9}. As listas têm entre 1 e 1000 notas de 0 a 20. Define uma operação de impressão que conserve o mapa, escreva nome e média por média decrescente, desempate por nome crescente e termine com a média das médias. Formata os números com duas casas decimais; para o mapa vazio, escreve Sem estudantes.
Prevê a saída completa e explica dois erros propostos: guardar cada nome num map<double, string> cuja chave é a média e calcular o valor global somando todas as notas e dividindo pelo número de notas. É necessário arredondar as médias antes de ordenar?
Solução
As médias são 15 para Duarte, 15 para Beatriz e 9 para Caio. O desempate por nome põe Beatriz antes de Duarte. A saída é:
Beatriz 15.00
Duarte 15.00
Caio 9.00
Global 13.00Um vetor de resumos conserva um par nome/média por estudante. O comparador usa média maior primeiro, e nome menor quando as médias são iguais. Um mapa cuja chave é 15 não conserva dois valores distintos para essa mesma chave; acabaria por omitir um dos estudantes.
A média das médias é (15 + 15 + 9) / 3 = 13. A outra conta dá (18 + 12 + 15 + 9 + 9 + 9) / 6 = 12: dá peso triplo a Caio face a Beatriz. São medidas diferentes. Usa acumulação real, ordena e calcula o total sem arredondar os valores intermédios. fixed e setprecision(2) só formatam a impressão. No mapa vazio, não existe média para dividir; aplica a mensagem definida pelo contrato.
Confere a tua resposta:
- Conservei os três estudantes, ordenados por média e depois por nome.
- Obtive média das médias 13 e distingui a média de todas as notas.
- Calculei antes de formatar e tratei o mapa vazio sem dividir por zero.