Durante años, procesar una colección en C++ significaba escribir bucles explÃcitos, variables temporales y condiciones dispersas por el código. Con std::ranges de C++20 ese patrón cambia: describes qué quieres hacer con los datos, no cómo iterar sobre ellos.
El resultado no es solo más legible. Al componerse como pipelines perezosos, las vistas evitan crear contenedores intermedios, lo que en colecciones grandes puede marcar la diferencia en memoria y en caché.
El operador pipe y las vistas
La interfaz central de std::ranges son las vistas (views), que viven en el namespace std::views. Se encadenan con el operador | igual que en Unix:
#include <ranges>
#include <vector>
#include <print>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto pipeline = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);
for (int n : pipeline) {
std::println("{}", n); // 4, 16, 36
}
}
Nada de esto construye vectores intermedios. El pipeline es perezoso: solo procesa los elementos que el bucle final consume. Si take(3) se satisface antes de recorrer todo el vector, los elementos restantes no se procesan.
Vistas esenciales de C++20
std::views::filter
// Solo strings no vacÃos
auto no_vacios = palabras | std::views::filter([](const std::string& s) {
return !s.empty();
});
std::views::transform
// Convertir a mayúsculas (simplificado)
auto upper = strs | std::views::transform([](std::string s) {
for (char& c : s) c = std::toupper(c);
return s;
});
std::views::take y std::views::drop
auto primeros_5 = rango | std::views::take(5); auto sin_primero = rango | std::views::drop(1); auto pagina = rango | std::views::drop(20) | std::views::take(10);
std::views::reverse e iota
#include <ranges>
// Iterar al revés sin modificar el contenedor
for (int n : nums | std::views::reverse) { /* ... */ }
// Generar secuencias numéricas sin vector
for (int i : std::views::iota(0, 100)) { /* ... */ }
std::views::split y std::views::join
#include <ranges>
#include <string_view>
std::string_view csv = "uno,dos,tres,cuatro";
for (auto token : csv | std::views::split(',')) {
// token es un subrange de chars
std::println("{}", std::string_view(token));
}
Algoritmos de rango: sin iteradores begin/end
Además de las vistas, std::ranges incluye versiones de todos los algoritmos clásicos que aceptan directamente un rango completo, sin necesidad de pasar .begin() y .end():
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 3, 1, 4, 2};
std::ranges::sort(v); // ordena in-place
std::ranges::sort(v, std::greater<>{}); // descendente
auto it = std::ranges::find(v, 3); // busca el 3
bool ok = std::ranges::all_of(v, [](int n){ return n>0; });
La mayorÃa también admiten una proyección, que es una función que se aplica a cada elemento antes de comparar:
struct Persona { std::string nombre; int edad; };
std::vector<Persona> personas = { {"Ana", 30}, {"Carlos", 25}, {"Bea", 28} };
// Ordenar por edad sin lambda de comparación compleja
std::ranges::sort(personas, {}, &Persona::edad);
Novedades de C++23 en rangos
std::views::zip
Itera sobre varias colecciones al mismo tiempo, produciendo tuplas de elementos:
#include <ranges>
#include <print>
std::vector<std::string> nombres = {"Ana", "Bob", "Carlos"};
std::vector<int> edades = {30, 25, 28};
for (auto [nombre, edad] : std::views::zip(nombres, edades)) {
std::println("{} tiene {} años", nombre, edad);
}
std::views::enumerate
for (auto [i, val] : std::views::enumerate(nombres)) {
std::println("[{}] {}", i, val);
}
std::views::chunk y std::views::slide
// Dividir en grupos de 3
for (auto grupo : nums | std::views::chunk(3)) {
// grupo es un subrange de 3 elementos
}
// Ventana deslizante de tamaño 2
for (auto ventana : nums | std::views::slide(2)) {
// ventana tiene los elementos [i, i+1]
}
std::views::cartesian_product
std::vector<int> filas = {0, 1, 2};
std::vector<int> cols = {0, 1, 2};
for (auto [f, c] : std::views::cartesian_product(filas, cols)) {
std::println("({}, {})", f, c); // todos los pares
}
Rangos y std::generator juntos
Con std::generator de C++23 puedes crear rangos propios mediante corrutinas y encadenarlos con las vistas estándar. El artÃculo sobre corrutinas de esta serie lo cubre en detalle, pero el patrón básico es:
#include <generator>
#include <ranges>
std::generator<int> primos() {
// generador de números primos
co_yield 2;
for (int n = 3; ; n += 2) {
bool es_primo = true;
for (int d = 3; d * d <= n; d += 2)
if (n % d == 0) { es_primo = false; break; }
if (es_primo) co_yield n;
}
}
// Los primeros 10 primos mayores que 100
auto resultado = primos()
| std::views::drop_while([](int n){ return n <= 100; })
| std::views::take(10);
Cuándo usar rangos y cuándo no
Los rangos brillan cuando el pipeline de transformaciones es claro y las colecciones son grandes. Para bucles simples de una sola operación, un for clásico sigue siendo perfectamente legible. Y hay casos donde los rangos tienen limitaciones: las vistas de escritura (output ranges) aún no están completas en C++23, y algunos adaptadores no funcionan sobre rangos no-bidireccionales.
Si vienes del mundo funcional o conoces las pipelines de Rust con iteradores, te encontrarás en terreno familiar. La diferencia principal es que en C++ las vistas no consumen el rango original.
Para operaciones más complejas sobre datos, la combinación de std::ranges con std::format o std::print (cubiertos en el artÃculo de novedades de C++23) da como resultado código muy compacto y sin boilerplate.
Imagen: Pexels / Myburgh Roux
