Pare de Escrever Laços — a Biblioteca <algorithm>

Pare de Escrever Laços — a Biblioteca <algorithm>

Escrever o laço à mão conta como percorrer; chamar o algoritmo diz o que se quer. Aqui estão sort, find, count, any_of, transform e accumulate aplicados a casos concretos, mais o idioma erase-remove — que confunde justamente porque remove não remove nada, apenas reorganiza e devolve onde termina a parte útil.
Linguagem C++

12 min de leitura

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 accumulate e 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.

Comentários

Mais em Linguagem C++

O Projeto Final Começa — Arquitetando um Mini Banco de Dados em Memória
O Projeto Final Começa — Arquitetando um Mini Banco de Dados em Memória

O projeto final não traz conceito novo — cobra os que já apareceram, todos ao…

A Disciplina do const — Promessas que o Compilador Cobra
A Disciplina do const — Promessas que o Compilador Cobra

Em C o const é etiqueta modesta, que muita gente ignora sem prejuízo. Em C++…

Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr
Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr

Quando várias partes do programa precisam legitimamente compartilhar um…