Percorrendo com Elegância — o Range-Based for a Fundo

Percorrendo com Elegância — o Range-Based for a Fundo

A sintaxe curta esconde uma decisão que custa caro se for tomada por descuido: percorrer por valor copia cada elemento, e para strings ou structs grandes isso é uma alocação por iteração. O texto percorre as três formas idiomáticas, o caso dos maps com structured bindings e o que o laço vira por baixo.
Linguagem C++

11 min de leitura

Fechamos o artigo Pare de Escrever Laços — a Biblioteca <algorithm> dominando a biblioteca <algorithm>, e ao longo de toda a Fase 4 usei, dezenas de vezes, uma sintaxe que nunca expliquei formalmente: o for (int x : v). Chegou a hora de fechar essa lacuna e, com ela, a Fase 4. O range-based for — o "for para intervalos", introduzido no C++11 — é a forma mais limpa de percorrer um container, e você já o viu funcionar. Mas há mais nele do que a sintaxe agradável sugere: ele se apoia diretamente nos iteradores dos artigos O Fio que Une Tudo — Iteradores como Conceito Central e Pare de Escrever Laços — a Biblioteca <algorithm>, esconde uma armadilha de cópia que pode custar desempenho, e tem formas idiomáticas que todo programador C++ deve ter no reflexo. Dominá-lo amarra containers, iteradores e algoritmos num modelo mental único — o fecho perfeito para a fase da STL.

A sintaxe e o que ela realmente faz

O range-based for percorre todos os elementos de um container sem que você mencione iteradores, índices ou tamanhos:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {10, 20, 30};

    // "para cada int x em v": x recebe uma CÓPIA de cada elemento, um por vez.
    for (int x : v)
        std::cout << x << ' ';   // 10 20 30
    std::cout << '\n';
    return 0;
}

Isso é açúcar sintático: o compilador o traduz, nos bastidores, para um laço com iteradores — algo equivalente a for (auto it = v.begin(); it != v.end(); ++it) { int x = *it; ... }. Ou seja, tudo que você aprendeu sobre begin()/end() no artigo O Fio que Une Tudo — Iteradores como Conceito Central está embaixo dessa sintaxe. Por isso o range-based for funciona sobre qualquer coisa que tenha begin()/end(): vector, string, map, set, arrays de C e até seus próprios tipos, se você lhes der esses métodos. É a mesma unificação por iteradores, agora com uma roupa elegante.

A armadilha central: cópia silenciosa

Aqui está o ponto que separa quem usa o range-based for por acaso de quem o usa com domínio — e eu já plantei essa semente lá no artigo Adeus NULL, Bem-vindo à Dedução — nullptr e auto. Escrever for (int x : v) faz x ser uma cópia de cada elemento. Para int, isso é trivial. Mas para elementos caros — strings, vetores, objetos grandes — copiar cada um a cada iteração é desperdício silencioso:

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> nomes = {"Ana", "Bernardo", "Cristina"};

    // RUIM: cada iteração COPIA a string inteira para 'nome'. Desperdício.
    for (std::string nome : nomes)
        std::cout << nome << ' ';
    std::cout << '\n';

    // BOM: 'const auto&' é uma referência só-leitura — nenhuma cópia.
    for (const auto& nome : nomes)
        std::cout << nome << ' ';
    std::cout << '\n';
    return 0;
}

A regra de ouro, que você deve gravar como reflexo: para apenas ler, use const auto&. Isso evita a cópia e ainda promete não modificar o elemento. É exatamente a regra do artigo Adeus NULL, Bem-vindo à Dedução — nullptr e auto (auto copia, const auto& referencia) aplicada ao percurso — e é uma das otimizações mais fáceis e frequentes do C++ real. Programadores experientes escrevem for (const auto& x : container) no automático.

As três formas idiomáticas

Há três variações, cada uma para um propósito. Saber qual usar é parte do vocabulário:

#include <iostream>
#include <vector>
#include <string>

int main() {
    std::vector<std::string> v = {"a", "b", "c"};

    // 1) Só LER (o caso mais comum): const auto& — sem cópia, sem modificação.
    for (const auto& s : v)
        std::cout << s;
    std::cout << '\n';

    // 2) MODIFICAR os elementos: auto& — referência mutável ao elemento real.
    for (auto& s : v)
        s += "!";   // altera cada elemento DENTRO do vetor
    for (const auto& s : v) std::cout << s;   // a! b! c!
    std::cout << '\n';

    // 3) Precisar de uma CÓPIA independente (raro): auto (por valor).
    for (auto s : v) {
        s += "?";   // modifica só a cópia local; o vetor NÃO muda
    }
    for (const auto& s : v) std::cout << s;   // a! b! c! (inalterado)
    std::cout << '\n';
    return 0;
}

O resumo mental: const auto& para ler (padrão), auto& para modificar no lugar, auto (por valor) só quando você realmente precisa de uma cópia que possa alterar sem afetar o original. Na prática, const auto& cobre a grande maioria dos casos.

Percorrendo maps: o par e os structured bindings

Percorrer um map ou unordered_map merece nota, porque cada elemento é um par. A forma clássica usa .first/.second; a forma moderna (C++17) usa structured bindings, que desempacotam o par em duas variáveis nomeadas — muito mais legível:

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

int main() {
    std::map<std::string, int> idade = {{"Ana", 30}, {"Bruno", 25}};

    // Clássico: acessar .first (chave) e .second (valor)
    for (const auto& par : idade)
        std::cout << par.first << " tem " << par.second << " anos\n";

    // Moderno (C++17): structured bindings desempacotam o par.
    // 'const auto&' continua valendo: sem cópia do par.
    for (const auto& [nome, anos] : idade)
        std::cout << nome << " tem " << anos << " anos\n";
    return 0;
}

O [nome, anos] é muito mais claro que par.first/par.second. Usei essa forma de relance no artigo Buscas Instantâneas — map, set e suas Versões Hash; agora ela está formalmente apresentada. Structured bindings são um dos recursos do C++17 que mais melhoram a legibilidade no dia a dia, e voltaremos a eles com profundidade na Fase 6.

O range-based for esconde os iteradores, mas não as consequências de escolher mal a forma: auto x copia cada elemento, auto& permite modificar, const auto& lê sem copiar. Para tipos baratos a diferença é irrelevante; para std::string ou structs grandes é uma alocação por iteração, silenciosa e fácil de não perceber. O padrão que evita quase todo desperdício é começar por const auto& e só abrir mão dele quando houver motivo.

Com isto se fecha a STL, e o que fica não é a lista de containers, é o modelo: containers guardam, iteradores percorrem, algoritmos operam, e o range-based for costura os três numa sintaxe curta. Entrar nesta fase implementando estruturas de dados à mão e sair usando as prontas é a diferença mais prática entre escrever C e escrever C++.

Fontes e leituras recomendadas

  • cppreference.com/w/cpp/language/range-for: a especificação do range-based for, incluindo a tradução para iteradores e as regras de captura de referência.
  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), seções sobre percurso de containers: a apresentação do range-based for como forma idiomática.
  • Scott Meyers, Effective Modern C++ (2014), Item 13 (preferir const_iterator) e os itens sobre auto: a base para a regra const auto&.
  • cppreference.com/w/cpp/language/structured_bindings: a referência dos structured bindings usados para percorrer maps.
  • ISO C++ Core Guidelines, regras ES.71 ("Prefer a range-for-statement to a for-statement when there is a choice") e ES.55: as diretrizes oficiais sobre quando e como usar o range-based for.

Exercícios

Exercício 1

Percorra um std::vector<int> com range-based for e imprima cada elemento. Depois use auto& para dobrar cada elemento no lugar e imprima de novo, comprovando que o vetor mudou.

Ver resposta

✓ Resposta: Ler e depois modificar:

#include <iostream>
#include <vector>
int main() {
    std::vector<int> v = {1, 2, 3};
    for (const auto& x : v) std::cout << x << ' ';   // 1 2 3
    std::cout << '\n';

    for (auto& x : v) x *= 2;   // auto& → modifica no lugar
    for (const auto& x : v) std::cout << x << ' ';   // 2 4 6
    std::cout << '\n';
    return 0;
}

O auto& dá uma referência ao elemento real, então x *= 2 altera o vetor; com auto (por valor) as mudanças ficariam na cópia local.

Exercício 2

Explique a diferença concreta entre for (auto x : v), for (auto& x : v) e for (const auto& x : v) para um std::vector<std::string>. Qual é o padrão para leitura e por quê?

Ver resposta

✓ Resposta: Para um std::vector<std::string>: for (auto x : v) copia cada string para x a cada iteração (desperdício, mas permite modificar a cópia sem afetar o vetor); for (auto& x : v) dá uma referência mutável ao elemento real, permitindo modificá-lo dentro do vetor; for (const auto& x : v) dá uma referência só-leitura, sem cópia e sem permissão de modificar. O padrão para leitura é const auto&, porque evita a cópia (importante para elementos caros como strings) e documenta a intenção de não modificar — o melhor dos dois mundos quando você só precisa observar.

Exercício 3

O laço abaixo é correto mas ineficiente. Aponte o problema de desempenho e corrija-o com uma única mudança.

#include <vector>
#include <string>
#include <iostream>
std::vector<std::string> textos = {"um texto longo", "outro texto", "mais um"};
for (std::string t : textos)
    std::cout << t.size() << '\n';
Ver resposta

✓ Resposta: O problema: for (std::string t : textos) copia cada string inteira para t a cada iteração, só para chamar .size() e descartá-la — uma alocação e cópia desnecessárias por elemento. Como o laço só , a correção é uma referência só-leitura:

for (const auto& t : textos)
    std::cout << t.size() << '\n';

Nenhuma cópia ocorre; t apenas referencia cada elemento.

Exercício 4

Use structured bindings num range-based for para percorrer um std::map<std::string, double> de produtos e preços, imprimindo "produto: preço". Compare a legibilidade com a versão usando .first/.second.

Ver resposta

✓ Resposta: Com structured bindings:

#include <iostream>
#include <map>
#include <string>
int main() {
    std::map<std::string, double> precos = {{"pão", 0.50}, {"leite", 4.20}};
    // Moderno e legível:
    for (const auto& [produto, preco] : precos)
        std::cout << produto << ": " << preco << '\n';
    // Versus clássico:
    for (const auto& par : precos)
        std::cout << par.first << ": " << par.second << '\n';
    return 0;
}

A versão com [produto, preco] é mais legível porque nomeia o que cada parte significa — quem lê entende de imediato que a chave é o produto e o valor é o preço. A versão com .first/.second obriga o leitor a lembrar que first é a chave e second o valor, o que é menos claro e mais sujeito a confusão em pares aninhados. Os structured bindings tornam a intenção explícita.

Exercício 5

Explique por que o range-based for funciona igualmente sobre um std::vector, um std::set e um array de C (int a[] = {1,2,3};), ligando a resposta ao conceito de iteradores e ao par begin()/end() do artigo O Fio que Une Tudo — Iteradores como Conceito Central.

Ver resposta

✓ Resposta: O range-based for funciona sobre os três porque, nos bastidores, ele se traduz para um laço baseado em begin()/end() e iteradores. Um std::vector e um std::set fornecem métodos begin()/end() que devolvem iteradores; um array de C (int a[]) é suportado por sobrecargas especiais de std::begin(a)/std::end(a) que produzem ponteiros para o início e o fim do array (e ponteiros são a forma mais primitiva de iterador). Em todos os casos, o que o range-based for precisa é apenas de um "começo", um "fim" e a capacidade de avançar (++) e desreferenciar (*) — exatamente o contrato de iterador do artigo O Fio que Une Tudo — Iteradores como Conceito Central. Como os três honram esse contrato (o array através das funções livres std::begin/std::end), a mesma sintaxe de percurso serve a todos, indiferente à estrutura interna. É a unificação por iteradores, mais uma vez, sustentando a elegância da sintaxe.

Comentários

Mais em Linguagem C++

Imagem e Som — Recursos de Jogo com RAII, e o Início do Snake
Imagem e Som — Recursos de Jogo com RAII, e o Início do Snake

Textura e som entram na mesma lista de arquivo, memória e conexão: alguém…

Buscas Instantâneas — map, set e suas Versões Hash
Buscas Instantâneas — map, set e suas Versões Hash

Nem todo problema é sobre sequências; muitos são sobre associações, e em C…

Um Nome, Vários Sentidos — Sobrecarga de Funções
Um Nome, Vários Sentidos — Sobrecarga de Funções

Ter três nomes para valor absoluto não era capricho da biblioteca do C: dois…