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.

Você vai aprender
  • A forma curta dos algoritmos: std::ranges::sort(v), sem begin/end.
  • O que é uma view e as cinco mais usadas: filter, transform, take, reverse e iota.
  • 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::to C++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 membro nota de Aluno".
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
ViewO que mostraParecido com
std::views::filter(pred)Só os elementos em que a lambda responde truestd::copy_if, if dentro do for
std::views::transform(f)O resultado de f em cada elementostd::transform
std::views::take(n)Os n primeiros (ou menos, se não houver)break depois de n voltas
std::views::reverseOs 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-1for (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çoCom views
Vetor extraSim (aprovadas), com cópiasNão: nada é copiado
Trabalho feitoPassa por todas as notasPara assim que tem 3
LeituraVocê precisa "rodar o laço na cabeça"Cada etapa tem um nome: filtrar, transformar, pegar
Quando é melhorLógica com muitos if/else misturados, ou para quem ainda não conhece viewsEtapas 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:

  1. Montar o pipeline não imprimiu nada: as lambdas não tinham rodado.
  2. Os números foram um de cada vez pela esteira inteira: filtrou o 2, entregou o 2, só depois filtrou o 3.
  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 o take conferir 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
A view não é dona dos dados

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.

O programa nunca termina com 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

EscritaCabeçalhoO 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:

std::(v);

2. O cano

Qual símbolo liga uma view a outra?

auto r = v std::views::take(2);

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 misturar begin/end de 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++20 ou mais; o guia usa -std=c++23.