Translate

lunes, 7 de septiembre de 2026

¿Qué significa que una operación sea lazy en C++?



Cuando trabajamos con Ranges y Views en C++, muchas operaciones son lazy.

Pero, ¿qué significa exactamente?

Significa que una operación no se ejecuta inmediatamente.


Veamos un ejemplo:

#include <ranges>

#include <vector>

#include <iostream>


int main() {

    std::vector<int> numbers = {1, 2, 3, 4, 5};


    auto result = numbers

        | std::views::filter([](int x) {

            std::cout << "Filtering: " << x << '\n';

            return x % 2 == 0;

        });


    std::cout << "Pipeline created\n";

}


Podríamos esperar que al crear result se ejecutara el filter.

Pero no.


La salida será: Pipeline created

El filtro todavía no se ejecutó.


¿Por qué?

Porque esto:

auto result = numbers

    | std::views::filter(...);


no procesa los elementos.


Simplemente crea una View que describe cómo obtenerlos.

La operación se ejecuta cuando necesitamos los valores.


Por ejemplo:

for (auto value : result) {

    std::cout << value << '\n';

}


Ahora sí veremos:

Filtering: 1

Filtering: 2

2

Filtering: 3

Filtering: 4

4

Filtering: 5


El filter se ejecuta a medida que recorremos los elementos.

Una operación eager procesa los datos inmediatamente.


Por ejemplo:

std::vector<int> result;


for (int value : numbers) {

    if (value % 2 == 0) {

        result.push_back(value);

    }

}


Cuando termina este código, result ya contiene:

[2, 4]


Los elementos fueron procesados inmediatamente.

Con una View:

auto result =

    numbers

    | std::views::filter([](int x) {

        return x % 2 == 0;

    });


todavía no tenemos una nueva colección.


Tenemos una forma de decir: Cuando alguien recorra estos datos, devolvé solamente los números pares.


¿Por qué es útil? 

Supongamos este pipeline:

auto result = numbers

    | std::views::filter(isEven)

    | std::views::transform(multiplyByTen)

    | std::views::take(2);


Podemos leerlo así:

numbers

   |

filter pares

   |

multiplicar por 10

   |

tomar 2


Como las operaciones son lazy, C++ no necesita necesariamente procesar toda la colección.


Si solamente necesitamos:

2 elementos


el pipeline puede detenerse cuando los encuentra.


Por ejemplo:


[1, 2, 3, 4, 5, 6, 7, 8]


        ↓


filter pares


[2, 4, 6, 8]


        ↓


transform x * 10


[20, 40, 60, 80]


        ↓


take(2)


[20, 40]


La diferencia es que una evaluación eager podría procesar todos los elementos antes de aplicar take.

Con una View, las operaciones pueden realizarse solamente cuando son necesarias.


Las operaciones trabajan elemento por elemento. Esta es una de las cosas más interesantes de los pipelines lazy.


Veamos:


auto result = numbers

    | std::views::filter([](int x) {

        std::cout << "filter: " << x << '\n';

        return x % 2 == 0;

    })

    | std::views::transform([](int x) {

        std::cout << "transform: " << x << '\n';

        return x * 10;

    })

    | std::views::take(2);


Cuando recorremos el resultado:

for (auto value : result) {

    std::cout << "result: " << value << '\n';

}


El procesamiento ocurre más parecido a esto:

filter: 1

filter: 2

transform: 2

result: 20

filter: 3

filter: 4

transform: 4

result: 40


No necesariamente ocurre así:


  filtrar todos

        ↓

transformar todos

        ↓

tomar dos


Sino más parecido a:


elemento

   ↓

filter

   ↓

transform

   ↓

resultado


Y después:


siguiente elemento

   ↓

filter

   ↓

transform

   ↓

resultado


Lazy no significa que nunca se ejecuta, una View no evita el procesamiento.

Simplemente lo retrasa hasta que alguien necesita los datos.


Por ejemplo:


auto result = numbers

    | std::views::filter(isEven);


Hasta acá:

No procesamos los elementos


Pero cuando hacemos:


for (auto value : result) {

    // ...

}



ahí comienza la evaluación.


Podemos resumirlo así:


Crear View

    ↓

No procesar todavía

    ↓

Recorrer los datos

    ↓

Procesar elementos cuando son necesarios


Podemos pensar una View como una receta.


Esto:

auto result =

    numbers

    | std::views::filter(isEven)

    | std::views::transform(multiplyByTen);


no es el resultado final.


Es más parecido a decir: Tengo estos datos. Cuando alguien los necesite, primero filtrá los pares y después multiplicalos por diez.

La ejecución real ocurre cuando alguien consume los elementos.


Cuando una operación es lazy significa que:

No procesa los elementos inmediatamente.

Describe cómo obtener o transformar los datos.

Se ejecuta cuando alguien necesita los resultados.

Puede evitar trabajo innecesario.

Permite construir pipelines eficientes.


En C++ moderno, las Views nos permiten escribir:


numbers

    | filter

    | transform

    | take;


sin crear necesariamente una colección intermedia en cada paso.

Y quizás esa sea la idea más importante: Una View no es el resultado. Es una forma de llegar al resultado.


No hay comentarios.:

Publicar un comentario