Lambdas en C++: Closures, Capturas y Flexibilidad

Una lambda en C++ no es simplemente una “función anónima”; es una clausura (closure). Cuando escribes una lambda, el compilador genera una clase única y anónima (un functor) cuyo operador () ha sido sobrecargado para ejecutar el cuerpo de la lambda. El “estado” que la lambda retiene —las variables que capturas— se almacena como miembros privados de esa clase generada.

Este diseño permite que las lambdas sean extremadamente eficientes: el compilador conoce el tipo exacto de la clausura en el punto de uso, lo que facilita la inlining (evitando el salto indirecto de un puntero a función) y permite optimizaciones que con std::function o punteros a función tradicionales serían imposibles. Usarás lambdas principalmente para pasar lógica personalizada a algoritmos de la STL, para definir callbacks rápidos o para encapsular lógica local que no merece tener un nombre propio en el espacio de nombres global. Sin embargo, si capturas algo por referencia [&] y la lambda sobrevive más allá del ámbito de la variable original, entrarás en el terreno del undefined behavior (UB) debido a una referencia colgante (dangling reference).

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include <concepts>

struct ProcesadorDeDatos {
    int factor = 2;

    // Lambda con captura de objeto por valor [ *this ] [C++17]
    // Evita problemas de vida útil si la lambda se mueve a otro hilo.
    auto obtener_copia_estado() const {
        return [*this](int val) {
            return val * this->factor;
        };
    }

    // Lambda genérica [C++14]: El operador() es un template implícito
    auto sumar_genérico = [](auto a, auto b) {
        return a + b;
    };

    void ejecutar_ejemplos() {
        std::vector<int> numeros = {5, 2, 8, 1, 9};

        // 1. Uso con algoritmos y captura por referencia [&]
        int suma = 0;
        std::for_each(numeros.begin(), numeros.end(), [&suma](int n) {
            suma += n;
        });
        std::cout << "Suma (captura [&]): " << suma << "\n";

        // 2. Lambda mutable: Permite modificar la captura por valor [=]
        int contador = 0;
        auto incrementador = [contador]() mutable {
            return ++contador;
        };
        std::cout << "Contador 1: " << incrementador() << "\n"; // 1
        std::cout << "Contador 2: " << incrementador() << "\n"; // 2
        // Nota: El 'contador' original en 'ejecutar_ejemplos' sigue siendo 0.

        // 3. Ordenamiento personalizado
        std::sort(numeros.begin(), numeros.end(), [](int a, int b) {
            return a > b; 
        });

        // 4. Lambda recursiva usando std::function
        // Necesitamos std::function para que la lambda se conozca a sí misma.
        std::function<int(int)> factorial = [&](int n) -> int {
            return (n <= 1) ? 1 : n * factorial(n - 1);
        };
        std::cout << "Factorial de 5: " << factorial(5) << "\n";
    }
};

int main() {
    ProcesadorDeDatos proc;
    proc.ejecutar_ejemplos();

    // Ejemplo de constexpr [C++17]
    constexpr auto cuadrado = [](int n) { return n * n; };
    static_assert(cuadrado(5) == 25);

    // Ejemplo de lambda genérica con diferentes tipos
    proc.sumar_genérico(10, 20);       // int
    proc.sumar_genérico(1.5, 2.5);     // double
    proc.sumar_genérico(std::string("A"), std::string("B")); // std::string

    return 0;
}

Análisis del código

En ejecutar_ejemplos, la lambda pasada a std::for_each utiliza captura por referencia [&suma]. Esto significa que la clausura generada contiene un puntero o referencia interna a suma, permitiendo modificar la variable local de ejecutar_ejemplos.

La lambda incrementador utiliza la cláusula mutable. Por defecto, el operador () de una lambda es const, lo que impide modificar las variables capturadas por valor. Sin mutable, intentar ++contador dentro de la lambda daría un error de compilación. Al añadirlo, le indicamos al compilador que el operador () puede ser no-constante, permitiéndonos alterar la copia interna de contador que reside en la clausura.

La lambda sumar_genérico es una lambda genérica introducida en [C++14]. Aunque no la veas, el compilador genera algo similar a una función de plantilla: template<typename T, typename U> auto operator()(T a, U b) const. Esto permite que la misma instancia de la lambda funcione con int, double o incluso tipos personalizados, siempre que soporten el operador +.

En el caso de obtener_copia_estado, usamos [*this] [C++17]. Si usáramos [this], estaríamos capturando un puntero al objeto actual. Si el objeto ProcesadorDeDatos se destruye antes de que la lambda se ejecute (por ejemplo, en un entorno asíncrono), la lambda intentaría acceder a memoria invalidada. [*this] crea una copia completa del objeto dentro de la clausura, garantizando que la lambda sea segura de usar de forma independiente.

Finalmente, la lambda recursiva requiere std::function porque el tipo de la lambda es anónimo y el compilador no puede deducir su propio tipo para la llamada recursiva dentro del cuerpo. Esto introduce un pequeño overhead debido al almacenamiento en el heap y la indirección del puntero a función de std::function, a diferencia de una función normal o una lambda no recursiva.

El error frecuente

Un error clásico es la referencia colgante al intentar devolver una lambda que captura variables locales por referencia.

// ERROR: El objeto devuelto contiene una referencia a una variable local que morirá.
auto crear_lambda_peligrosa() {
    int x = 42;
    return [&x](int val) { return x + val; }; 
}

int main() {
    auto f = crear_lambda_peligrosa();
    // UB: 'x' ya no existe en el stack. El programa puede crashear o dar basura.
    std::cout << f(10) << std::endl; 
}

Si intentas compilar esto con -Wdangling-lambda en GCC o Clang, el compilador te avisará del peligro. Para evitarlo, captura por valor [x] o usa std::shared_ptr si la vida del objeto es incierta. El uso de AddressSanitizer (-fsanitize=address) detectará este error en tiempo de ejecución de forma inmediata.

67

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio