Views, Evaluacion Perezosa y Composicion
Domina las views de C++20 — adaptadores de rango ligeros y perezosos que se componen con el operador pipe. Aprende filter, transform, take, drop, split, join, iota y como construir potentes pipelines de datos sin asignaciones intermedias.
Que Son las Views?
Una view es un rango ligero que es:
1. No propietario — no posee los elementos que referencia
2. Copia y movimiento O(1) — las views son baratas de pasar
3. Evaluadas perezosamente — los elementos se calculan bajo demanda durante la iteracion, no por adelantado
Las views son los bloques de construccion de los pipelines de rango. En lugar de crear contenedores intermedios en cada paso, las views se componen en un unico paso sobre los datos:
// Tradicional: crea 2 vectores intermedios
auto evens = filter(data, is_even); // asignacion
auto squared = transform(evens, sq); // asignacion
// Views: cero asignaciones intermedias
auto result = data | views::filter(is_even) | views::transform(sq);
// Nada calculado aun — result es solo un descriptor de vista
for (int x : result) { ... } // calculado elemento por elementoLas views viven en std::views:: (un alias de namespace para std::ranges::views::).
filter y transform — Las Views Fundamentales
views::filter conserva los elementos que coinciden con un predicado. views::transform aplica una funcion a cada elemento. Juntos, reemplazan la mayoria de los bucles explicitos.
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter even numbers, then square them
auto even_squares = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
// Nothing has been computed yet — even_squares is a view
for (int x : even_squares) {
std::cout << x << ' '; // 4 16 36 64 100
}
std::cout << '\n';
// Views work with any range — including strings
std::string text = "Hello, World!";
auto uppercase = text
| std::views::filter([](char c) { return std::isalpha(c); })
| std::views::transform([](char c) { return std::toupper(c); });
for (char c : uppercase) {
std::cout << c; // HELLOWORLD
}
std::cout << '\n';
// Collect view results into a vector (C++23: std::ranges::to)
std::vector<int> collected;
for (int x : even_squares) {
collected.push_back(x);
}
// C++23: auto collected = even_squares | std::ranges::to<std::vector>();
}take, drop y Views de Segmentacion
views::take(n) produce los primeros n elementos. views::drop(n) omite los primeros n elementos. Juntos permiten una segmentacion eficiente sin copiar.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// take — first N elements
for (int x : data | std::views::take(3)) {
std::cout << x << ' '; // 1 2 3
}
std::cout << '\n';
// drop — skip first N elements
for (int x : data | std::views::drop(7)) {
std::cout << x << ' '; // 8 9 10
}
std::cout << '\n';
// Combine: elements 3-7 (0-indexed: drop 3, take 4)
for (int x : data | std::views::drop(3) | std::views::take(4)) {
std::cout << x << ' '; // 4 5 6 7
}
std::cout << '\n';
// take_while / drop_while — predicate-based
std::vector<int> sorted_data = {1, 3, 5, 7, 2, 4, 6};
auto ascending_prefix = sorted_data | std::views::take_while(
[prev = 0](int x) mutable {
bool ok = x > prev;
prev = x;
return ok;
});
for (int x : ascending_prefix) {
std::cout << x << ' '; // 1 3 5 7
}
std::cout << '\n';
// Pagination example: page 2, page_size = 3
int page = 2, page_size = 3;
auto page_view = data
| std::views::drop((page - 1) * page_size)
| std::views::take(page_size);
for (int x : page_view) {
std::cout << x << ' '; // 4 5 6
}
std::cout << '\n';
}split, join y Views de Procesamiento de Strings
views::split divide un rango por un delimitador. views::join aplana rangos anidados. Son potentes para el procesamiento de strings y datos.
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include <iostream>
int main() {
// split — divide a string by delimiter
std::string csv = "Alice,30,Engineer,95000";
for (auto field : csv | std::views::split(',')) {
// Each 'field' is a subrange — convert to string_view
std::string_view sv(field.begin(), field.end());
std::cout << '[' << sv << "] ";
}
std::cout << '\n'; // [Alice] [30] [Engineer] [95000]
// join — flatten nested ranges
std::vector<std::vector<int>> matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
for (int x : matrix | std::views::join) {
std::cout << x << ' '; // 1 2 3 4 5 6 7 8 9
}
std::cout << '\n';
// Combine: split lines, then process each line
std::string multiline = "hello world\nfoo bar\nbaz qux";
int line_num = 1;
for (auto line : multiline | std::views::split('\n')) {
std::string_view line_sv(line.begin(), line.end());
std::cout << line_num++ << ": " << line_sv << '\n';
}
// Enumerate (C++23)
// for (auto [i, val] : data | std::views::enumerate) { ... }
}views::iota — Generando Rangos
views::iota genera una secuencia de valores incrementales. Puede ser acotado (iota(0, 10) para 0..9) o no acotado (iota(0) para 0, 1, 2, ...). Combinado con take, reemplaza muchos bucles de conteo.
#include <ranges>
#include <iostream>
#include <vector>
#include <numeric>
int main() {
// Bounded iota — replaces for(int i = 0; i < 10; ++i)
for (int x : std::views::iota(0, 10)) {
std::cout << x << ' '; // 0 1 2 3 4 5 6 7 8 9
}
std::cout << '\n';
// Unbounded iota + take — first 5 squares
auto first_5_squares = std::views::iota(1)
| std::views::transform([](int x) { return x * x; })
| std::views::take(5);
for (int x : first_5_squares) {
std::cout << x << ' '; // 1 4 9 16 25
}
std::cout << '\n';
// Generate Fibonacci-like sequence with views
// (iota as index generator)
auto indices = std::views::iota(0, 20);
auto even_indices = indices | std::views::filter([](int i) { return i % 2 == 0; });
for (int i : even_indices | std::views::take(5)) {
std::cout << i << ' '; // 0 2 4 6 8
}
std::cout << '\n';
// Use iota to create a vector of sequential values
auto range_vec = std::views::iota(1, 11); // 1 to 10
int sum = 0;
for (int x : range_vec) sum += x;
std::cout << "Sum 1..10: " << sum << '\n'; // 55
}Construyendo Pipelines de Datos Completos
El verdadero poder de las views emerge cuando compones multiples adaptadores en un unico pipeline. Todo el pipeline se evalua perezosamente en un solo paso.
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
struct LogEntry {
std::string level; // "INFO", "WARN", "ERROR"
std::string message;
int timestamp;
};
int main() {
std::vector<LogEntry> logs = {
{"INFO", "Server started", 1000},
{"ERROR", "Connection refused", 1001},
{"INFO", "Request received", 1002},
{"WARN", "Slow query detected", 1003},
{"ERROR", "Timeout expired", 1004},
{"INFO", "Request completed", 1005},
{"ERROR", "Disk full", 1006},
{"WARN", "Memory usage high", 1007},
};
// Pipeline: get recent error messages, formatted
auto recent_errors = logs
| std::views::filter([](const LogEntry& e) {
return e.level == "ERROR";
})
| std::views::drop(1) // skip first error
| std::views::transform([](const LogEntry& e) {
return "[" + std::to_string(e.timestamp) + "] " + e.message;
})
| std::views::take(5); // at most 5 results
std::cout << "Recent errors (after first):\n";
for (const auto& msg : recent_errors) {
std::cout << " " << msg << '\n';
}
// [1004] Timeout expired
// [1006] Disk full
// Reverse view — iterate backward without copying
auto reversed = logs | std::views::reverse | std::views::take(3);
std::cout << "\nLast 3 entries (reversed):\n";
for (const auto& e : reversed) {
std::cout << " [" << e.level << "] " << e.message << '\n';
}
}Las views son no propietarias — referencian el rango subyacente. Si el rango fuente se destruye, la view queda colgante:
auto make_view() {
std::vector<int> local = {1, 2, 3};
return local | std::views::filter([](int x) { return x > 1; });
// BUG: local se destruye, la view queda colgante!
}Tambien ten cuidado con las views sobre temporales creados en medio del pipeline:
// INCORRECTO: el string es un temporal que se destruye
auto words = std::string("hello world")
| std::views::split(' '); // colgante!Siempre asegurate de que los datos fuente sobrevivan a todas las views que los referencian. Almacena los datos en una variable con tiempo de vida suficiente antes de crear views sobre ellos.
Las views son perezosas — no se realiza ningun trabajo hasta la iteracion. Esto significa:
1. Construir un pipeline es esencialmente gratuito (solo almacena punteros a funciones y parametros)
2. Puedes crear views sobre rangos muy grandes o incluso infinitos
3. Cada elemento fluye a traves de todo el pipeline antes de que el siguiente elemento sea procesado
4. Las views de cortocircuito como take(n) detienen todo el pipeline despues de n elementos
Para materializar los resultados de una view en un contenedor, itera y recolecta:
std::vector<int> results;
for (int x : some_view) results.push_back(x);
// C++23 agrega std::ranges::to para una materializacion mas limpia:
auto results = some_view | std::ranges::to<std::vector>();Prefiere mantener los datos como views el mayor tiempo posible — solo materializa cuando necesites acceso aleatorio, persistencia o pasar a APIs que requieran contenedores.
- Las views son adaptadores de rango no propietarios, copiables en O(1) y evaluados perezosamente
views::filteryviews::transformson los bloques de construccion fundamentales para pipelines de datosviews::take(n)yviews::drop(n)proporcionan segmentacion sin copiarviews::iotagenera secuencias — acotadas o no acotadas — reemplazando bucles de conteo- El operador pipe
|compone views en pipelines legibles de izquierda a derecha con cero asignaciones intermedias - Las views no deben sobrevivir a sus datos fuente — siempre asegura que el rango subyacente tenga tiempo de vida suficiente
Quiz — Pon a prueba tus conocimientos
(20 XP)1. Cuando calcula una view de C++20 sus elementos?
2. Cual es el resultado de `std::views::iota(1) | std::views::take(3)`?
3. Por que es peligroso crear una view sobre una variable local y devolverla desde una funcion?