C++ Ranges C++20
Na aula de Algoritmos, todo algoritmo recebia v.begin(), v.end(). Funciona, mas é repetitivo, e fazer uma tarefa em etapas ("pegue as notas aprovadas, some 1 ponto, fique com as 3 primeiras") exige vetores intermediários e laços. O C++20 trouxe os ranges ("intervalos"): algoritmos que recebem o vetor inteiro de uma vez e views ("vistas") que você encaixa uma na outra com o símbolo |, como canos de uma tubulação.
- A forma curta dos algoritmos:
std::ranges::sort(v), sembegin/end. - O que é uma view e as cinco mais usadas:
filter,transform,take,reverseeiota. - Como encadear views com
|, e por que se diz que elas são "preguiçosas". - Como transformar o resultado num vetor de verdade com
std::ranges::toC++23. - A comparação antes × depois: o mesmo programa com laço e com ranges.
Tudo foi conferido com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. O -std=c++23 é obrigatório aqui: o g++ 15 usa C++17 se você não pedir outro padrão, e o C++17 não tem ranges (veja "Quando dá errado"). Para esta aula você precisa de Lambdas e Algoritmos.
A ideia: a esteira da fábrica
Imagine uma esteira de frutas. As frutas passam por estações: a primeira separa as estragadas (um filtro), a segunda descasca (uma transformação), a última pega só 3 para a caixa. Ninguém descasca todas as frutas antes de começar a encher a caixa: cada fruta anda pela esteira inteira, uma de cada vez, e a máquina para quando a caixa enche.
As views são essas estações, e o | (a barra vertical, chamada "pipe", cano) é a esteira que liga uma à outra:
auto caixa = frutas
| std::views::filter(boa) // separa
| std::views::transform(descascar) // descasca
| std::views::take(3); // só 3
1. Algoritmos sem begin e end
Quase todo algoritmo de <algorithm> tem uma versão "irmã" dentro de std::ranges, que recebe o container direto:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas = {7, 10, 4, 8, 6};
std::ranges::sort(notas);
for (int n : notas) std::cout << n << ' ';
std::cout << '\n';
auto it = std::ranges::find(notas, 8);
if (it != notas.end()) {
std::cout << "Achei o 8 na posição " << (it - notas.begin()) << '\n';
}
auto aprovados = std::ranges::count_if(notas, [](int n) { return n >= 6; });
std::cout << "Aprovados: " << aprovados << '\n';
}
$ g++ -std=c++23 -Wall -Wextra ordenar.cpp -o ordenar $ ./ordenar 4 6 7 8 10 Achei o 8 na posição 3 Aprovados: 4
| Antes (clássico) | Depois (ranges) |
|---|---|
std::sort(notas.begin(), notas.end()); | std::ranges::sort(notas); |
std::find(notas.begin(), notas.end(), 8) | std::ranges::find(notas, 8) |
std::count_if(notas.begin(), notas.end(), f) | std::ranges::count_if(notas, f) |
std::reverse(v.begin(), v.end()); | std::ranges::reverse(v); |
A versão de ranges não é só mais curta: ela também evita um erro clássico, passar o begin de um vetor e o end de outro (std::sort(a.begin(), b.end())), que compila sem aviso e é comportamento indefinido. Com std::ranges::sort(a), não há como misturar. O find continua devolvendo um iterador, e você continua comparando com end(), igual à aula anterior. A versão com begin/end continua válida e muito usada; o cabeçalho continua sendo <algorithm>.
Ordenar por um campo: a projeção
Os algoritmos de ranges aceitam um último argumento chamado projeção: "compare os elementos olhando só este campo". Ordenar alunos pela nota, que antes pedia uma lambda de dois parâmetros, fica assim:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Aluno {
std::string nome;
double nota;
};
int main() {
std::vector<Aluno> turma = {{"Bruno", 7.5}, {"Ana", 9.0}, {"Carla", 6.0}};
std::ranges::sort(turma, {}, &Aluno::nota);
for (const auto& a : turma) std::cout << a.nome << ' ' << a.nota << '\n';
auto melhor = std::ranges::max_element(turma, {}, &Aluno::nota);
std::cout << "Melhor: " << melhor->nome << '\n';
}
$ g++ -std=c++23 -Wall -Wextra projecao.cpp -o projecao $ ./projecao Carla 6 Bruno 7.5 Ana 9 Melhor: Ana
- turma
- O que ordenar.
- {}
- A comparação. As chaves vazias querem dizer "a de sempre" (menor primeiro). Para maior primeiro, coloque
std::ranges::greater{}. - &Aluno::nota
- A projeção: "de cada aluno, olhe o campo
nota". Lê-se "o membronotadeAluno". - melhor->nome
melhoré um iterador; a seta->pega um campo do elemento para onde ele aponta (é o mesmo que(*melhor).nome).
2. Views: olhar os dados de outro jeito, sem copiar
Uma view não guarda elementos. Ela é uma "janela" que mostra os elementos de outro container filtrados, transformados, cortados ou ao contrário, sem copiar e sem mudar o original. As views ficam no cabeçalho <ranges>, dentro de std::views.
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5, 6, 7, 8};
auto pares = numeros | std::views::filter([](int n) { return n % 2 == 0; });
for (int n : pares) std::cout << n << ' ';
std::cout << '\n';
auto dobro = numeros | std::views::transform([](int n) { return n * 2; });
for (int n : dobro) std::cout << n << ' ';
std::cout << '\n';
for (int n : numeros | std::views::take(3)) std::cout << n << ' ';
std::cout << '\n';
for (int n : numeros | std::views::reverse) std::cout << n << ' ';
std::cout << '\n';
for (int n : std::views::iota(1, 6)) std::cout << n << ' ';
std::cout << '\n';
std::cout << "Original: ";
for (int n : numeros) std::cout << n << ' ';
std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra filtrar.cpp -o filtrar $ ./filtrar 2 4 6 8 « filter: só os que passam no teste 2 4 6 8 10 12 14 16 « transform: cada um vezes 2 1 2 3 « take(3): os 3 primeiros 8 7 6 5 4 3 2 1 « reverse: de trás para frente 1 2 3 4 5 « iota(1, 6): de 1 até 5 (o 6 fica de fora) Original: 1 2 3 4 5 6 7 8 « o vetor não mudou
| View | O que mostra | Parecido com |
|---|---|---|
std::views::filter(pred) | Só os elementos em que a lambda responde true | std::copy_if, if dentro do for |
std::views::transform(f) | O resultado de f em cada elemento | std::transform |
std::views::take(n) | Os n primeiros (ou menos, se não houver) | break depois de n voltas |
std::views::reverse | Os elementos de trás para frente (sem parênteses!) | std::reverse, mas sem mexer no original |
std::views::iota(a, b) | Os números a, a+1, …, b-1 | for (int i = a; i < b; ++i) |
O nome iota vem da letra grega ι, usada para sequências de números na linguagem APL, dos anos 1960. Ela é diferente das outras: não olha para um container, ela gera os números. Com um só argumento, std::views::iota(1), a sequência não tem fim (1, 2, 3, … para sempre); veremos já já como isso pode ser útil.
Experimente: rode e veja os números ímpares. Depois troque a condição do filtro para pegar os maiores que 4. Em seguida, troque o filter por std::views::transform([](int n) { return n * n; }) para ver os quadrados.
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5, 6, 7, 8};
auto escolhidos = numeros | std::views::filter([](int n) { return n % 2 == 1; });
for (int n : escolhidos) {
std::cout << n << ' ';
}
std::cout << '\n';
}
3. Encadear com |
A graça das views está em ligar várias. O resultado de uma view pode entrar em outra, quantas vezes você quiser:
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> notas = {7, 3, 10, 5, 8, 4, 9};
// as 3 primeiras notas aprovadas, com 1 ponto de bônus
auto resultado = notas
| std::views::filter([](int n) { return n >= 6; })
| std::views::transform([](int n) { return n + 1; })
| std::views::take(3);
for (int n : resultado) std::cout << n << ' ';
std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra encadear.cpp -o encadear $ ./encadear 8 11 9 « 7, 10 e 8 passaram; +1 em cada; parou no 3º
Leia de cima para baixo, como uma receita: "das notas, filtre as maiores ou iguais a 6, some 1 a cada uma, pegue 3". A ordem das etapas importa: take(3) antes do filter pegaria as 3 primeiras notas (7, 3, 10) e só depois filtraria, sobrando 7 e 10.
Antes × depois
O mesmo programa sem ranges, como se escrevia antes do C++20:
std::vector<int> aprovadas;
for (int n : notas) {
if (n >= 6) {
aprovadas.push_back(n + 1);
}
}
if (aprovadas.size() > 3) {
aprovadas.resize(3);
}
$ ./antigo 8 11 9 « mesmo resultado
| Com laço | Com views | |
|---|---|---|
| Vetor extra | Sim (aprovadas), com cópias | Não: nada é copiado |
| Trabalho feito | Passa por todas as notas | Para assim que tem 3 |
| Leitura | Você precisa "rodar o laço na cabeça" | Cada etapa tem um nome: filtrar, transformar, pegar |
| Quando é melhor | Lógica com muitos if/else misturados, ou para quem ainda não conhece views | Etapas simples em sequência |
Nenhuma das duas formas é "errada". Views brilham em sequências de etapas simples; para lógica complicada, um laço bem escrito costuma ser mais claro.
4. Preguiçosas: nada acontece até você pedir
Criar uma view não calcula nada. As lambdas só rodam quando alguém pede um elemento (no for, por exemplo). Isso se chama avaliação preguiçosa (lazy). Para ver acontecer, coloque um cout dentro da lambda do filtro:
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto pipeline = numeros
| std::views::filter([](int n) {
std::cout << " filtrando " << n << '\n';
return n % 2 == 0;
})
| std::views::take(2);
std::cout << "Pipeline montado. Nada foi calculado ainda.\n";
for (int n : pipeline) {
std::cout << "Recebi " << n << '\n';
}
}
$ g++ -std=c++23 -Wall -Wextra preguica.cpp -o preguica
$ ./preguica
Pipeline montado. Nada foi calculado ainda. « nenhum "filtrando" antes daqui
filtrando 1
filtrando 2
Recebi 2 « o 2 passou e já foi entregue ao for
filtrando 3
filtrando 4
Recebi 4
filtrando 5 « o filtro andou até o próximo par…
filtrando 6 « …e só então o take viu que já tinha 2
« 7, 8, 9 e 10 nunca foram olhados
Três coisas para notar:
- Montar o pipeline não imprimiu nada: as lambdas não tinham rodado.
- Os números foram um de cada vez pela esteira inteira: filtrou o 2, entregou o 2, só depois filtrou o 3.
- O
take(2)fez o trabalho parar cedo. Repare que o filtro ainda olhou o 5 e o 6: ao avançar depois do 4, ele procura o próximo par antes de otakeconferir que já entregou 2. É um detalhe de funcionamento; o importante é que 7, 8, 9 e 10 ficaram de fora.
Por causa da preguiça, dá para usar a sequência sem fim do iota, desde que algo corte:
#include <iostream>
#include <ranges>
int main() {
auto quadrados = std::views::iota(1)
| std::views::transform([](int n) { return n * n; })
| std::views::take(5);
for (int q : quadrados) std::cout << q << ' ';
std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra infinito.cpp -o infinito $ ./infinito 1 4 9 16 25
Uma view só "olha" para o vetor original. Se o vetor for destruído ou mudar de tamanho enquanto a view existe, a view passa a olhar para memória inválida. Regra prática: crie a view, use-a logo em seguida (num for) e não a guarde para depois. Se precisar guardar o resultado, transforme-o num vetor, como na próxima seção.
5. std::ranges::to: de view para vetor C++23
Uma view não é um vetor: não dá para fazer push_back nela, nem guardá-la depois que o original sumir. Para "materializar" o resultado num container de verdade, o C++23 trouxe std::ranges::to, que vai no fim da esteira:
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5, 6};
auto pares = numeros
| std::views::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>();
pares.push_back(100);
for (int n : pares) std::cout << n << ' ';
std::cout << "(tamanho " << pares.size() << ")\n";
}
$ g++ -std=c++23 -Wall -Wextra para_vetor.cpp -o para_vetor $ ./para_vetor 2 4 6 100 (tamanho 4) « agora é um std::vector<int> de verdade
Repare em std::ranges::to<std::vector>(): você diz só "vetor", e o compilador descobre que é de int. Confirmado no g++ 15 com -std=c++23. Antes do C++23, a saída era um laço com push_back, ou criar o vetor a partir dos iteradores da view.
Quando dá errado
error: ‘std::ranges’ has not been declared
$ g++ ordenar.cpp -o ordenar17
ordenar.cpp: In function ‘int main()’:
ordenar.cpp:8:10: error: ‘std::ranges’ has not been declared
8 | std::ranges::sort(notas);
| ^~~~~~
ordenar.cpp:12:20: error: ‘std::ranges’ has not been declared
12 | auto it = std::ranges::find(notas, 8);
| ^~~~~~
ordenar.cpp:17:27: error: ‘std::ranges’ has not been declared
17 | auto aprovados = std::ranges::count_if(notas, [](int n) { return n >= 6; });
| ^~~~~~
O que significa: "std::ranges não foi declarado". Sem -std=, o g++ 15 compila como C++17, que não tem ranges.
Como consertar: compile com -std=c++20 ou, como no guia, -std=c++23. Para views, confira também o #include <ranges>.
error: conversion from ‘std::ranges::filter_view<…>’ to non-scalar type ‘std::vector<int>’ requested
$ g++ -std=c++23 -Wall -Wextra sem_to.cpp -o sem_to
sem_to.cpp: In function ‘int main()’:
sem_to.cpp:6:38: error: conversion from ‘std::ranges::filter_view<std::ranges::ref_view<std::vector<int> >, main()::<lambda(int)> >’ to non-scalar type ‘std::vector<int>’ requested
6 | std::vector<int> pares = numeros | std::views::filter([](int n) { return n % 2 == 0; });
| ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
O que significa: "pediu-se uma conversão de filter_view para std::vector<int>", e ela não existe. A view é uma janela, não um vetor. O tipo dela é esse nome enorme, e é por isso que se usa auto.
Como consertar: guarde a view em auto, ou acrescente | std::ranges::to<std::vector>() no fim.
iota
O que significa: std::views::iota(1) sem take (ou sem um break no for) gera números para sempre.
Como consertar: use iota(a, b) com fim, ou ponha | std::views::take(n) no pipeline. Se travar no terminal, aperte Ctrl+C.
Referência rápida
| Escrita | Cabeçalho | O que faz |
|---|---|---|
std::ranges::sort(v) | <algorithm> | Ordena o container inteiro |
std::ranges::sort(v, {}, &T::campo) | <algorithm> | Ordena olhando um campo (projeção) |
std::ranges::find, count_if, max_element, reverse… | <algorithm> | Os algoritmos clássicos, recebendo o container |
v | std::views::filter(pred) | <ranges> | Só os que passam |
v | std::views::transform(f) | <ranges> | f aplicada em cada um |
v | std::views::take(n) | <ranges> | Os n primeiros |
v | std::views::reverse | <ranges> | De trás para frente |
std::views::iota(a, b) / iota(a) | <ranges> | a, a+1, …, b-1 / sem fim |
… | std::ranges::to<std::vector>() | <ranges> | Vira um vetor de verdade (C++23) |
Há muitas outras views (drop, take_while, split, join, enumerate, zip…); elas estão em Referência: Algoritmos.
Exercícios
1. Forma curta
Complete para ordenar o vetor v com a forma de ranges:
2. O cano
Qual símbolo liga uma view a outra?
3. O que mostra for (int n : std::views::iota(3, 7)) std::cout << n << ' ';?
4. Depois de auto r = v | std::views::transform([](int n) { return n * 10; });, o que aconteceu com v?
5. Com v = {1, 2, 3, 4, 5, 6}, o que dá v | std::views::take(3) | std::views::filter([](int n) { return n % 2 == 0; })?
Missão: do clássico para ranges
Missão: o programa funciona, mas usa a forma antiga. Reescreva as duas chamadas com std::ranges::sort e std::ranges::count_if, passando frutas direto (sem begin/end). A saída tem de continuar a mesma.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> frutas = {"uva", "banana", "pera", "abacaxi", "kiwi"};
std::sort(frutas.begin(), frutas.end());
auto curtas = std::count_if(frutas.begin(), frutas.end(), [](const std::string& f) {
return f.size() <= 4;
});
for (const auto& f : frutas) std::cout << f << ' ';
std::cout << '\n';
std::cout << "Curtas: " << curtas << '\n';
}
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> frutas = {"uva", "banana", "pera", "abacaxi", "kiwi"};
std::ranges::sort(frutas);
auto curtas = std::ranges::count_if(frutas, [](const std::string& f) {
return f.size() <= 4;
});
for (const auto& f : frutas) std::cout << f << ' ';
std::cout << '\n';
std::cout << "Curtas: " << curtas << '\n';
}
Missão final: múltiplos de 7 ao quadrado
Missão: monte um pipeline que comece em std::views::iota(1, 31), filtre os múltiplos de 7 (n % 7 == 0), eleve cada um ao quadrado e pegue os 3 primeiros. A saída deve ser 49 196 441.
#include <iostream>
#include <ranges>
int main() {
auto resultado = std::views::iota(1, 31);
// encaixe aqui o filter, o transform e o take
for (int n : resultado) std::cout << n << ' ';
std::cout << '\n';
}
#include <iostream>
#include <ranges>
int main() {
auto resultado = std::views::iota(1, 31)
| std::views::filter([](int n) { return n % 7 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);
for (int n : resultado) std::cout << n << ' ';
std::cout << '\n';
}
A solução, no laboratório:
$ g++ -std=c++23 -Wall -Wextra setes.cpp -o setes $ ./setes 49 196 441
Resumo
std::ranges::sort(v),std::ranges::find(v, x)etc. recebem o container inteiro: menos digitação e sem misturarbegin/endde vetores diferentes.- Projeção:
std::ranges::sort(turma, {}, &Aluno::nota)ordena por um campo. - Views (
<ranges>) são janelas sobre os dados: não copiam e não mudam o original. As principais:filter,transform,take,reverse,iota. - O
|encadeia views; leia da esquerda para a direita (ou de cima para baixo). - Views são preguiçosas: só calculam quando alguém pede, um elemento por vez, e podem parar cedo.
| std::ranges::to<std::vector>()(C++23) transforma o resultado num vetor de verdade.- Ranges exigem
-std=c++20ou mais; o guia usa-std=c++23.