No artigo O Fio que Une Tudo — Iteradores como Conceito Central você entendeu os iteradores como a língua franca da STL — a interface que qualquer container fala e que qualquer algoritmo entende. Hoje colhemos a recompensa. A biblioteca <algorithm> é uma coleção de dezenas de operações prontas — ordenar, buscar, transformar, somar, contar, copiar, filtrar — que funcionam sobre qualquer container via iteradores. A promessa é direta: para tarefas comuns, você deixa de escrever laços for à mão e passa a chamar uma função testada, nomeada e otimizada. Isso não é só menos digitação; é código que comunica intenção em vez de mecânica, com menos espaço para bugs. Ao fim da aula, você olhará para um laço manual e se perguntará se não há um algoritmo pronto para ele — e quase sempre haverá.
Ordenar: sort
Comece pelo mais famoso. Em C, ordenar exigia escrever ou chamar qsort com um ponteiro de função de comparação desajeitado. Em C++, std::sort ordena um intervalo em uma linha:
#include <iostream>
#include <vector>
#include <algorithm> // std::sort
int main() {
std::vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end()); // ordena em ordem crescente, in-place
for (int x : v) std::cout << x << ' '; // 1 2 3 5 8 9
std::cout << '\n';
// Ordem decrescente: passe um comparador. std::greater<int>{} inverte.
std::sort(v.begin(), v.end(), std::greater<int>{});
for (int x : v) std::cout << x << ' '; // 9 8 5 3 2 1
std::cout << '\n';
return 0;
}
std::sort recebe o intervalo [begin, end) — os iteradores da aula passada — e ordena. Opcionalmente, um terceiro argumento diz como comparar. Note que sort exige iteradores de acesso aleatório (os saltos que mencionei no O Fio que Une Tudo), então funciona com vector mas não com set — que, aliás, já é ordenado e não precisa. Tudo se encaixa.
Buscar e contar: find, count, any_of
Vários algoritmos respondem perguntas sobre o conteúdo:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {4, 8, 15, 16, 23, 42};
// find: devolve iterador para o primeiro igual (ou end()).
auto it = std::find(v.begin(), v.end(), 16);
std::cout << (it != v.end() ? "achou 16\n" : "não achou\n");
// count: quantos elementos são iguais a um valor.
std::vector<int> notas = {7, 8, 7, 10, 7};
std::cout << "setes: " << std::count(notas.begin(), notas.end(), 7) << '\n'; // 3
// any_of / all_of: testa uma condição sobre o intervalo.
// O último argumento é uma FUNÇÃO que devolve bool — aqui uma lambda (próximo tema).
bool tem_par = std::any_of(v.begin(), v.end(), [](int x){ return x % 2 == 0; });
std::cout << (tem_par ? "há pares\n" : "sem pares\n");
return 0;
}
Repare naquele [](int x){ return x % 2 == 0; } — uma lambda, função anônima escrita no local. Muitos algoritmos recebem uma função como argumento para dizer o que fazer (qual condição testar, como transformar). As lambdas são o assunto principal da Fase 5, mas elas aparecem tão naturalmente com algoritmos que vou usá-las desde já, explicando o mínimo: [](int x){ return ...; } é uma função sem nome que recebe um int x e devolve um bool. Por ora, leia-as como "a regra que passo ao algoritmo".
Transformar e reduzir: transform e accumulate
Dois algoritmos capturam padrões que você escreve o tempo todo à mão. std::transform aplica uma função a cada elemento, produzindo um novo intervalo — é o "mapa" da programação funcional. std::accumulate (que vive em <numeric>) combina todos os elementos num único valor — é a "redução", da qual somar é o caso mais comum:
#include <iostream>
#include <vector>
#include <algorithm> // std::transform
#include <numeric> // std::accumulate
int main() {
std::vector<int> v = {1, 2, 3, 4};
// transform: dobra cada elemento, escrevendo o resultado em 'dobrado'.
std::vector<int> dobrado(v.size()); // precisa ter espaço para o destino
std::transform(v.begin(), v.end(), dobrado.begin(),
[](int x){ return x * 2; });
for (int x : dobrado) std::cout << x << ' '; // 2 4 6 8
std::cout << '\n';
// accumulate: soma todos, começando de 0.
int soma = std::accumulate(v.begin(), v.end(), 0);
std::cout << "soma: " << soma << '\n'; // 10
// accumulate com operação customizada: produto, começando de 1.
int produto = std::accumulate(v.begin(), v.end(), 1,
[](int acc, int x){ return acc * x; });
std::cout << "produto: " << produto << '\n'; // 24
return 0;
}
Em C, cada um desses seria um laço com um acumulador e um índice — código que você lê linha a linha para descobrir a intenção. std::accumulate(v.begin(), v.end(), 0) diz "some tudo" de uma vez. Essa é a virada mental da aula: algoritmos nomeados tornam a intenção explícita.
O idioma erase-remove: removendo com algoritmos
Um caso que merece destaque porque é famoso e não-óbvio: remover elementos que satisfazem uma condição. No artigo O Fio que Une Tudo — Iteradores como Conceito Central vimos como fazer isso à mão com erase é traiçoeiro por causa da invalidação de iteradores. A STL oferece um idioma robusto, o erase-remove. std::remove_if empurra os elementos a manter para a frente e devolve o ponto onde começa o "lixo"; erase então corta o lixo:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// Remove todos os pares em duas etapas:
// 1) remove_if move os ímpares para a frente e devolve o novo "fim lógico".
// 2) erase corta tudo a partir dali.
auto novo_fim = std::remove_if(v.begin(), v.end(),
[](int x){ return x % 2 == 0; });
v.erase(novo_fim, v.end());
for (int x : v) std::cout << x << ' '; // 1 3 5
std::cout << '\n';
return 0;
}
É estranho no início, mas é o jeito correto e eficiente de remover por condição, sem os bugs de invalidação do laço manual. (No C++20, std::erase_if(v, pred) faz as duas etapas de uma vez — mais simples ainda; digo a versão, como sempre.)
Por que preferir algoritmos a laços manuais
Encerro com o argumento de fundo, e com honestidade sobre os limites. Preferir algoritmos a laços tem três ganhos reais. Primeiro, correção: std::sort e companhia são testados por milhões de programas; seu laço manual, não. Segundo, clareza: std::accumulate(...) comunica "reduzir a um valor" melhor que dez linhas de laço. Terceiro, otimização: implementações da STL costumam ser mais afiadas que o laço ingênuo. O limite honesto: nem todo laço tem um algoritmo pronto, e forçar um algoritmo onde um for simples seria mais legível é contraproducente. A diretriz é conhecer o catálogo e alcançá-lo quando ele encaixa — e a maioria dos laços comuns (ordenar, buscar, contar, transformar, somar, filtrar) encaixa. No C++20, as ranges deixam isso ainda mais limpo (std::ranges::sort(v) em vez de sort(v.begin(), v.end())), um tópico que tocaremos adiante.
Trocar o laço pelo algoritmo muda o que o código comunica: std::any_of diz o que se quer saber, enquanto o for equivalente conta como descobrir. O ganho não é digitar menos — é que a intenção fica no nome e os detalhes de percurso e limites saem de cena, junto com os erros que moram neles. O idioma erase-remove é o exemplo mais claro dessa troca: std::remove não remove nada, apenas reorganiza e devolve onde a parte útil termina, e é o erase que encurta o container de fato.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/algorithm: o catálogo completo da biblioteca
<algorithm>, com todos os algoritmos citados e dezenas de outros. - cppreference.com/w/cpp/algorithm/sort e /w/cpp/algorithm/transform: as referências detalhadas dos dois algoritmos centrais, com requisitos de iterador e complexidade.
- Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre algoritmos: a visão do criador sobre programar com algoritmos em vez de laços.
- Scott Meyers, Effective STL (2001), Itens 43 ("Prefer algorithm calls to hand-written loops") e 30–32: o argumento canônico desta aula.
- cppreference.com/w/cpp/numeric/accumulate: os detalhes de
accumulatee a família<numeric>, incluindo a versão com operação customizada.
Exercícios
Exercício 1
Dado um std::vector<int>, use std::sort para ordená-lo e depois std::accumulate para somar seus elementos. Imprima o vetor ordenado e a soma.
Ver resposta
✓ Resposta: Ordenar e somar:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main() {
std::vector<int> v = {5, 3, 9, 1, 7};
std::sort(v.begin(), v.end());
for (int x : v) std::cout << x << ' '; // 1 3 5 7 9
std::cout << '\n';
std::cout << "soma: " << std::accumulate(v.begin(), v.end(), 0) << '\n'; // 25
return 0;
}
Exercício 2
Use std::count_if para contar quantos elementos de um std::vector<int> são maiores que 10. (Dica: count_if recebe um intervalo e uma condição, como any_of.)
Ver resposta
✓ Resposta: Contagem condicional:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {5, 12, 8, 20, 3, 15};
int maiores = std::count_if(v.begin(), v.end(),
[](int x){ return x > 10; });
std::cout << "maiores que 10: " << maiores << '\n'; // 3
return 0;
}
count_if percorre o intervalo e conta os elementos para os quais a lambda devolve true.
Exercício 3
Use std::transform para gerar, a partir de um std::vector<std::string> de palavras, um novo vetor com o tamanho de cada palavra. Explique por que o vetor destino precisa ter tamanho suficiente antes da chamada.
Ver resposta
✓ Resposta: Tamanhos das palavras:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
int main() {
std::vector<std::string> palavras = {"oi", "mundo", "c++"};
std::vector<std::size_t> tamanhos(palavras.size()); // destino com espaço suficiente
std::transform(palavras.begin(), palavras.end(), tamanhos.begin(),
[](const std::string& s){ return s.size(); });
for (auto t : tamanhos) std::cout << t << ' '; // 2 5 3
std::cout << '\n';
return 0;
}
O vetor destino (tamanhos) precisa ter tamanho suficiente antes porque std::transform escreve nos elementos já existentes através do iterador tamanhos.begin() — ele não cria elementos novos. Se tamanhos estivesse vazio, transform escreveria além do fim, causando comportamento indefinido. (Alternativa: usar um std::back_inserter(tamanhos) como destino, que insere em vez de sobrescrever — um refinamento que a documentação detalha.)
Exercício 4
Reescreva o laço manual abaixo usando o idioma erase-remove (ou std::erase_if do C++20), e explique por que a versão com algoritmo evita o bug de invalidação de iterador.
#include <vector>
#include <string>
std::vector<std::string> v = {"ok", "erro", "ok", "falha", "ok"};
// Objetivo: remover todos os "ok".
Ver resposta
✓ Resposta: Com erase-remove e com C++20:
#include <vector>
#include <string>
#include <algorithm>
int main() {
std::vector<std::string> v = {"ok", "erro", "ok", "falha", "ok"};
// C++17: idioma erase-remove
v.erase(std::remove(v.begin(), v.end(), "ok"), v.end());
// C++20: uma linha
// std::erase(v, "ok");
return 0;
}
A versão com algoritmo evita o bug de invalidação porque std::remove faz todo o rearranjo de uma vez — ele move os elementos a manter para a frente e devolve o "fim lógico" —, e só então um único erase(inicio_lixo, end()) corta o excedente. Não há um laço em que você remove elementos individualmente enquanto o iterador ainda percorre, que era a fonte do comportamento indefinido do artigo O Fio que Une Tudo — Iteradores como Conceito Central. O rearranjo e o corte são separados e cada um opera sobre iteradores válidos.
Exercício 5
Um colega escreveu um laço for de 12 linhas que percorre um std::vector<double>, soma os valores positivos e conta quantos são. Mostre como expressar o somatório dos positivos com std::accumulate (usando uma lambda que só soma se positivo) e discuta o trade-off entre a versão com algoritmo e o laço manual quando duas coisas (soma e contagem) precisam sair do mesmo percurso.
Ver resposta
✓ Resposta: Somatório dos positivos com accumulate:
#include <numeric>
#include <vector>
double soma_positivos(const std::vector<double>& v) {
return std::accumulate(v.begin(), v.end(), 0.0,
[](double acc, double x){ return x > 0 ? acc + x : acc; });
}
O trade-off quando duas coisas precisam sair do mesmo percurso (soma e contagem): expressá-las como dois algoritmos separados (accumulate para a soma, count_if para a contagem) percorreria o vetor duas vezes — mais legível, porém potencialmente mais lento em coleções grandes. Um único laço for manual percorre uma vez só, calculando ambas de uma passada — mais eficiente, porém mais verboso e com a mecânica explícita. A decisão honesta: para coleções pequenas ou código não-crítico, prefira os dois algoritmos pela clareza; para o caminho quente onde o percurso duplo pesa, um laço manual (ou o std::transform_reduce do C++17, que combina transformação e redução numa passada) é justificável. É um caso onde "prefira algoritmos" cede a "meça e escolha" — a mesma honestidade de sempre sobre trade-offs.