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 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; }; // Copiar todo
auto allByRef = [&]() { x++; y++; }; // Referenciar todo
auto mixed = [x, &y]() { y += x; }; // Mezclar 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ó init captures (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 OOP cuando necesitas pasar comportamiento como parámetro —para callbacks, comparadores personalizados o controladores de eventos— sin definir objetos de función independientes.
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 devoluciones de llamada (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 las devoluciones de llamada de eventos.Tu despachador debe almacenar las devoluciones de llamada utilizando un
std::vectordestd::function<void()>. Incluye estos métodos:addCallback(std::function<void()> callback)— añade una devolución de llamada a la listafireAll()— invoca todas las devoluciones de llamada almacenadas en ordenclear()— elimina todas las devoluciones de llamada
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 todas las devoluciones de llamada e invocar cada una.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 devoluciones de llamada:- 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 todas las devoluciones de llamada, 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 la captura por referencia la afectó:
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 lee 1 en cada ronda. Esto se debe a que fireAll() itera sus devoluciones de llamada por valor: cada ronda invoca una copia fresca de la std::function almacenada, cuyo contador capturado comienza en 0 y se incrementa a 1, por lo que el estado propio de la devolución de llamada almacenada 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: Agregar una lambda que capture base POR VALOR
// Debería imprimir: "Base value: [base]"
// TODO: Agregar una lambda que capture multiplier POR REFERENCIA
// Debería incrementar multiplier en 1, luego imprimir: "Multiplier after increment: [multiplier]"
// TODO: Agregar una lambda MUTABLE que capture un contador (inicializado en 0) por valor
// Debería 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() nuevamente
// 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