Menu
Coddy logo textTech

Expresiones Lambda en profundidad

Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy. Lección 85 de 104.

Las expresiones lambda, introducidas en C++11, son funciones anónimas que puedes definir en línea. Aunque has visto lambdas básicas con algoritmos de STL, comprender su sintaxis completa permite aprovechar funcionalidades avanzadas para capturar variables y controlar cómo se accede a ellas.

La sintaxis completa de una lambda es: [capture](parameters) mutable -> return_type { body }. La cláusula de captura determina a qué variables externas puede acceder la lambda y cómo:

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    
    auto byValue = [x]() { return x * 2; };        // Copia de x
    auto byRef = [&y]() { y += 5; };               // Referencia a y
    auto allByValue = [=]() { return x + y; };     // Copia todos
    auto allByRef = [&]() { x++; y++; };           // Referencia todos
    auto mixed = [x, &y]() { y += x; };            // Mezcla ambos
    
    byRef();
    std::cout << y << "\n";  // 25
}

De forma predeterminada, las variables capturadas por valor son const dentro de la lambda. La palabra clave mutable permite modificar estas copias:

int counter = 0;
auto increment = [counter]() mutable {
    return ++counter;  // Modifica la copia de la lambda
};

std::cout << increment() << "\n";  // 1
std::cout << increment() << "\n";  // 2
std::cout << counter << "\n";      // 0 - original sin cambios

C++14 añadió las capturas de inicialización, lo que te permite crear nuevas variables o mover objetos dentro de la lambda:

auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
    return *p;
};  // Propiedad transferida a la lambda

Las lambdas son especialmente útiles en la POO cuando necesitas pasar un comportamiento como parámetro, por ejemplo, para callbacks, comparadores personalizados o manejadores de eventos, sin definir objetos de función independientes.

challenge icon

Desafío

Fácil

Construyamos un sistema de manejo de eventos que muestre el poder de las expresiones lambda con diferentes modos de captura. Crearás un despachador de eventos sencillo que almacena e invoca callbacks, demostrando cómo las lambdas pueden capturar estado externo de varias maneras.

Organizarás tu código en tres archivos:

  • EventDispatcher.h: Define una clase EventDispatcher que gestiona los callbacks de eventos.

    Tu despachador debe almacenar los callbacks usando un std::vector de std::function<void()>. Incluye estos métodos:

    • addCallback(std::function<void()> callback): añade un callback a la lista
    • fireAll(): invoca todos los callbacks almacenados en orden
    • clear(): elimina todos los callbacks

    Deberás incluir <functional> y <vector>.

  • EventDispatcher.cpp: Implementa los métodos de tu despachador. El método fireAll() simplemente debe iterar por todos los callbacks e invocar cada uno.
  • main.cpp: Lee dos entradas:
    1. Un número base (entero)
    2. Un multiplicador (entero)

    Crea un EventDispatcher y demuestra diferentes técnicas de captura de lambdas añadiendo tres callbacks:

    1. Una lambda que captura el número base por valor e imprime: Base value: [base]
    2. Una lambda que captura el multiplicador por referencia, lo incrementa en 1 y después imprime: Multiplier after increment: [multiplier]
    3. Una lambda mutable que captura una variable contador (inicializada en 0) por valor, la incrementa cada vez que se llama y muestra: Call count: [counter]

    Después de añadir todos los callbacks, llama a fireAll() dos veces para ver cómo se comportan los diferentes modos de captura en varias invocaciones. Entre las dos llamadas a fireAll(), imprime --- como separador.

    Finalmente, después de ambas rondas, imprime el valor final de la variable multiplicador de main para mostrar cómo le afectó la captura por referencia: Final multiplier: [multiplier]

Por ejemplo, con las entradas 10 y 5:

Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7

Con las entradas 42 y 0:

Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2

Observa los comportamientos clave: la captura por valor mantiene sin cambios el base original, la captura por referencia modifica la variable multiplicador real en main (acumulándose entre llamadas) y el contador de la lambda mutable muestra 1 en cada ronda. Esto se debe a que fireAll() itera por valor sobre sus callbacks: en cada ronda invoca una copia nueva del std::function almacenado, cuyo contador capturado comienza en 0 y se incrementa a 1, por lo que el estado propio del callback almacenado nunca avanza.

Pruébalo tú mismo

#include <iostream>
#include "EventDispatcher.h"

using namespace std;

int main() {
    int base;
    int multiplier;
    cin >> base;
    cin >> multiplier;
    
    EventDispatcher dispatcher;
    
    // TODO: Añade una lambda que capture base POR VALOR
    // Debería imprimir: "Base value: [base]"
    
    // TODO: Añade una lambda que capture multiplier POR REFERENCIA
    // It should increment multiplier by 1, then print: "Multiplier after increment: [multiplier]"
    
    // TODO: Añade una lambda MUTABLE que capture un contador (inicializado a 0) por valor
    // Debería incrementar el contador e imprimir: "Call count: [counter]"
    
    // TODO: Llama a fireAll() para invocar todos los callbacks
    
    // TODO: Imprime "---" como separador
    
    // TODO: Llama a fireAll() de nuevo
    
    // TODO: Print the final multiplier value: "Final multiplier: [multiplier]"
    
    return 0;
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos

Practica por tu cuenta: Compilador de C++ online