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. Si bien has visto lambdas básicas con algoritmos de la STL, comprender su sintaxis completa desbloquea capacidades potentes 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 todo
auto allByRef = [&]() { x++; y++; }; // Referencia todo
auto mixed = [x, &y]() { y += x; }; // Mezcla ambos
byRef();
std::cout << y << "\n"; // 25
}Por defecto, las variables capturadas por valor son const dentro de la lambda. La palabra clave mutable permite la modificación de 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 - el original no cambiaC++14 añadió capturas de inicialización, permitiéndote crear nuevas variables o mover objetos a la lambda:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Propiedad transferida a la lambdaLas lambdas son particularmente útiles en la POO cuando necesitas pasar comportamiento como un parámetro (para callbacks, comparadores personalizados o manejadores de eventos) sin definir objetos de función separados.
Desafío
FácilVamos a construir 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 simple que almacena e invoca callbacks, demostrando cómo las lambdas pueden capturar el estado externo de varias maneras.
Organizarás tu código en tres archivos:
EventDispatcher.h: Define una claseEventDispatcherque gestiona los callbacks de eventos.Tu despachador debe almacenar los callbacks usando un
std::vectordestd::function<void()>. Incluye estos métodos:addCallback(std::function<void()> callback)— añade un callback a la listafireAll()— invoca todos los callbacks almacenados en ordenclear()— elimina todos los callbacks
Necesitarás incluir
<functional>y<vector>.EventDispatcher.cpp: Implementa los métodos para tu despachador. El métodofireAll()simplemente debe iterar a través de todos los callbacks e invocar cada uno.main.cpp: Lee dos entradas:- Un número base (entero)
- Un multiplicador (entero)
Crea un
EventDispatchery demuestra diferentes técnicas de captura de lambda añadiendo tres callbacks:- Una lambda que capture el número base por valor e imprima:
Base value: [base] - Una lambda que capture el multiplicador por referencia, lo incremente en 1, y luego imprima:
Multiplier after increment: [multiplier] - Una lambda mutable que capture una variable contador (inicializada en 0) por valor, la incremente cada vez que sea llamada, e imprima:
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 a través de múltiples invocaciones. Entre las dos llamadas afireAll(), imprime---como separador.Finalmente, después de ambas rondas, imprime el valor final de la variable multiplicador desde 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: 7Con 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: 2Observa los comportamientos clave: la captura por valor mantiene la base original sin cambios, la captura por referencia modifica la variable multiplicador real en main (acumulándose entre llamadas), y el contador de la lambda mutable se comporta según cómo se almacenen y llamen los objetos lambda; observa cómo persiste el estado interno en tu implementación al usar std::function.
Hoja de referencia
Las expresiones lambda son funciones anónimas que pueden definirse en línea. La sintaxis completa es:
[capture](parameters) mutable -> return_type { body }Cláusulas de captura
La cláusula de captura determina a qué variables externas puede acceder la lambda y cómo:
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // Copy of x
auto byRef = [&y]() { y += 5; }; // Reference to y
auto allByValue = [=]() { return x + y; }; // Copy all variables
auto allByRef = [&]() { x++; y++; }; // Reference all variables
auto mixed = [x, &y]() { y += x; }; // Mix both modesLambdas mutables
Por defecto, las variables capturadas por valor son const dentro de la lambda. Use mutable para modificar las copias:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // Modifies the lambda's copy
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - original unchangedCapturas de inicialización (C++14)
Cree nuevas variables o mueva objetos dentro de la lambda:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // Ownership transferred into lambdaUso de lambdas con std::function
Almacene lambdas en std::function para callbacks y manejadores de eventos:
#include <functional>
#include <vector>
std::vector<std::function<void()>> callbacks;
callbacks.push_back([x]() { /* use x */ });
callbacks.push_back([&y]() { /* modify y */ });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ñadir una lambda que capture base POR VALOR
// Debe imprimir: "Base value: [base]"
// TODO: Añadir una lambda que capture multiplier POR REFERENCIA
// Debe incrementar multiplier en 1, luego imprimir: "Multiplier after increment: [multiplier]"
// TODO: Añadir una lambda MUTABLE que capture un contador (inicializado en 0) por valor
// Debe incrementar el contador e imprimir: "Call count: [counter]"
// TODO: Llamar a fireAll() para invocar todos los callbacks
// TODO: Imprimir "---" como separador
// TODO: Llamar a fireAll() de nuevo
// TODO: Imprimir el valor final de multiplier: "Final multiplier: [multiplier]"
return 0;
}
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
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico12Características de C++ moderno
Semántica de movimiento y RvaluesReenvío perfectoExpresiones Lambda en profundidadstd::function y std::bindconstexpr y constevalBindings estructuradosoptional, variant, any