C++20 Ranges: Un Nuevo Paradigma
Descubre los ranges de C++20 — un rediseno moderno de la interfaz de algoritmos de la STL. Aprende conceptos de range, algoritmos basados en ranges, proyecciones y el operador pipe para transformaciones de datos componibles.
Por Que Ranges?
Los Ranges de C++20 son una mejora fundamental al modelo de algoritmos de la STL. La STL tradicional requiere pasar pares de iteradores a cada algoritmo, lo cual es verboso y propenso a errores (puedes pasar accidentalmente iteradores de diferentes contenedores). Los ranges resuelven esto permitiendote pasar el contenedor en si — o cualquier objeto que defina begin() y end().
Mejoras clave sobre los algoritmos tradicionales:
1. Pasar contenedores directamente — std::ranges::sort(vec) en lugar de std::sort(vec.begin(), vec.end())
2. Proyecciones — transformar elementos antes de comparar, sin modificarlos
3. Concepts — los algoritmos tienen requisitos claros en tiempo de compilacion
4. Componibilidad — encadenar operaciones con el operador pipe |
5. Evaluacion perezosa — las vistas calculan elementos bajo demanda, evitando copias intermedias
Conceptos de Range
C++20 define una jerarquia de conceptos de range en :
- std::ranges::range — tiene begin() y end()
- std::ranges::sized_range — tiene size() en O(1)
- std::ranges::input_range — se puede leer una vez (como iteradores de entrada)
- std::ranges::forward_range — recorrido hacia adelante de multiples pasos
- std::ranges::bidirectional_range — adelante + atras
- std::ranges::random_access_range — bidireccional + saltos O(1)
- std::ranges::contiguous_range — acceso aleatorio + memoria contigua
- std::ranges::common_range — begin() y end() devuelven el mismo tipo
- std::ranges::viewable_range — se puede convertir a una vista
Estos conceptos se usan para restringir los algoritmos basados en ranges, dandote mensajes de error claros cuando pasas un range incompatible.
Algoritmos Basados en Ranges
Los algoritmos basados en ranges viven en std::ranges:: y aceptan contenedores enteros. Tambien devuelven tipos de resultado mas ricos en lugar de iteradores simples.
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
struct Employee {
std::string name;
int age;
double salary;
};
int main() {
std::vector<int> nums = {5, 3, 1, 4, 1, 5, 9, 2, 6};
// Pass the container directly — no begin/end needed
std::ranges::sort(nums);
// nums: {1, 1, 2, 3, 4, 5, 5, 6, 9}
// find returns a borrowed iterator (safe with temporary ranges)
auto it = std::ranges::find(nums, 4);
if (it != nums.end()) {
std::cout << "Found: " << *it << '\n';
}
// count_if with ranges
auto even_count = std::ranges::count_if(nums, [](int x) { return x % 2 == 0; });
std::cout << "Even count: " << even_count << '\n'; // 3
// min/max on entire range
auto [min_it, max_it] = std::ranges::minmax_element(nums);
std::cout << "Min: " << *min_it << ", Max: " << *max_it << '\n';
// Sorting employees by salary (descending)
std::vector<Employee> team = {
{"Alice", 30, 95000},
{"Bob", 25, 72000},
{"Charlie", 35, 110000},
};
std::ranges::sort(team, std::ranges::greater{}, &Employee::salary);
// Charlie (110k), Alice (95k), Bob (72k)
for (const auto& e : team) {
std::cout << e.name << ": $" << e.salary << '\n';
}
}Proyecciones — Transformar Antes de Comparar
Las proyecciones son una caracteristica unica de los algoritmos basados en ranges. Te permiten especificar como "ver" cada elemento para comparacion o procesamiento, sin modificar el elemento en si.
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>
int main() {
// Sort strings case-insensitively using a projection
std::vector<std::string> words = {"Banana", "apple", "Cherry", "date"};
std::ranges::sort(words, {}, [](const std::string& s) {
std::string lower = s;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](char c) { return std::tolower(c); });
return lower;
});
// words: {"apple", "Banana", "Cherry", "date"}
// Original strings preserved, sorted by lowercase version
for (const auto& w : words) std::cout << w << ' ';
std::cout << '\n';
// Using member pointer as projection
struct Point { int x, y; };
std::vector<Point> points = {{3, 1}, {1, 5}, {2, 3}};
// Sort by x coordinate
std::ranges::sort(points, {}, &Point::x);
// points: {{1,5}, {2,3}, {3,1}}
// Find max by y coordinate
auto max_y = std::ranges::max_element(points, {}, &Point::y);
std::cout << "Max y: (" << max_y->x << ", " << max_y->y << ")\n"; // (1, 5)
// Binary search with projection
bool found = std::ranges::binary_search(points, 2, {}, &Point::x);
std::cout << std::boolalpha << "Has x=2: " << found << '\n'; // true
}El Operador Pipe para Composicion
El operador pipe | es la marca distintiva de los ranges de C++20. Te permite encadenar adaptadores de rango (vistas) en un pipeline que se lee de izquierda a derecha, como un pipe de shell Unix:
auto result = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::take(5);Esto se lee naturalmente: "toma los numeros, conserva los pares, elevalos al cuadrado, toma los primeros 5." El pipeline es perezoso — ningun calculo ocurre hasta que iteras sobre result. Esto significa que no se asignan vectores intermedios.
Exploraremos las vistas en profundidad en la proxima leccion.
Los algoritmos de range protegen contra iteradores colgantes de ranges temporales. Si pasas un contenedor temporal, el iterador devuelto quedaria colgante porque el temporal se destruye en el punto y coma:
auto it = std::ranges::find(get_vector(), 42);
// Devuelve std::ranges::dangling en lugar de un iterador!
// Intentar desreferenciarlo no compilara.Esta es una caracteristica de seguridad en tiempo de compilacion — el std::find tradicional devolveria silenciosamente un iterador colgante, llevando a comportamiento indefinido en tiempo de ejecucion. El tipo centinela std::ranges::dangling convierte esto en un error de compilacion.
Prefiere algoritmos std::ranges:: en codigo nuevo — son mas seguros (sin iteradores desemparejados, proteccion contra colgantes), mas legibles (pasa contenedores directamente) y mas potentes (proyecciones, concepts).
Usa algoritmos tradicionales std:: cuando:
- Tu base de codigo apunta a compiladores pre-C++20
- Necesitas una sobrecarga especifica que ranges no proporciona
- Trabajas con pares de iteradores de APIs heredadas
Nota: los algoritmos std::ranges:: no son reemplazos directos. Algunos devuelven tipos diferentes (por ejemplo, std::ranges::sort devuelve el iterador final del rango ordenado). Revisa la documentacion al migrar.
- Los ranges de C++20 te permiten pasar contenedores directamente a algoritmos — se acabaron las plantillas de
begin()/end() - Las proyecciones transforman elementos para comparacion sin modificar los datos originales
- Los conceptos de range (
input_range,random_access_range, etc.) proporcionan restricciones claras en tiempo de compilacion - El operador pipe
|permite pipelines de procesamiento de datos componibles, de izquierda a derecha std::ranges::danglingpreviene errores de iteradores colgantes en tiempo de compilacion- Prefiere algoritmos
std::ranges::en codigo nuevo de C++20 por seguridad, legibilidad y potencia
Quiz — Pon a prueba tus conocimientos
(20 XP)1. Que es una proyeccion en el contexto de los algoritmos de range de C++20?
2. Que devuelve `std::ranges::find` cuando se llama con un contenedor temporal?
3. Que hace `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)`?