Una lambda en C++ no es solo una función anónima; es un objeto de tipo clase con un operator() generado por el compilador. Cuando escribes una lambda, el compilador crea una clase cuyo estado (las variables capturadas) son sus miembros de datos. Por eso, la naturaleza de la captura y la forma en que se definen sus parámetros determinan profundamente cómo se gestiona la memoria y cómo se optimiza el código.
Hasta C++20, si querías una lambda genérica, debías usar auto en los parámetros, lo cual te permitía tratar con cualquier tipo pero te impedía manipular el nombre del tipo directamente (por ejemplo, para usar std::is_same_v o declarar una variable del mismo tipo). Con la llegada de las lambda templates [C++20], ahora puedes usar la sintaxis de plantillas explícitas []<typename T>(T x). Esto permite realizar lógica de metaprogramulación dentro del cuerpo de la lambda, algo vital para bibliotecas de alto rendimiento.
En cuanto a la gestión de la vida del objeto, existe una distinción crítica entre capturar el puntero this [C++11] y capturar una copia del objeto [*this] [C++17]. Si capturas [this], la lambda guarda la dirección de memoria del objeto actual. Si esa lambda sobrevive al objeto (por ejemplo, si se pasa a un hilo o un std::async), tendrás un dangling pointer al intentar acceder a this. Por el contrario, [*this] invoca el constructor de copia y almacena una copia completa del objeto dentro de la lambda, garantizando seguridad de memoria a costa de un posible impacto en el rendimiento si el objeto es pesado.
Finalmente, la mutabilidad. Por defecto, el operator() de una lambda es const. Si intentas modificar una variable capturada por valor, el compilador fallará porque está intentando modificar un miembro de una función constante. La palabra clave mutable elimina esta restricción, permitiendo que el estado interno de la lambda cambie, aunque estos cambios no afectan al objeto original, ya que se operan sobre una copia interna.
#include <iostream>
#include <vector>
#include <type_traits>
#include <concepts>
#include <algorithm>
struct DataProcessor {
int multiplier = 2;
// C++20: Lambda con template explícito para inspección de tipos
auto get_type_checker() const {
return []<typename T>(T val) -> bool {
// Podemos usar T directamente gracias a la sintaxis de plantillas
using Type = std::decay_t<T>;
return std::is_integral_v<Type>;
};
}
// C++17: Captura segura [*this] para evitar dangling pointers en entornos async
void process_async_safe(auto lambda) const {
// Capturamos una copia del objeto actual para que la lambda
// sea segura incluso si 'this' desaparece.
auto safe_lambda = [*this](auto&& arg) {
lambda(arg, this->multiplier);
};
safe_lambda(10);
}
void demonstrate() const {
// C++20: Lambda con template y conceptos
auto generic_calc = []<typename T>(T val) constexpr -> T {
return val * 2;
};
// C++17: Lambda mutable para mantener estado interno
int internal_counter = 0;
auto incrementer = [internal_counter]() mutable {
return ++internal_counter;
};
// C++20: Recursión sin std::function usando el patrón "self-passing"
// Se pasa la propia lambda como argumento para evitar el overhead de std::function
auto factorial = [](auto self, int n) -> int {
return n <= 1 ? 1 : n * self(self, n - 1);
};
std::cout << "Template lambda (int): " << generic_calc(21) << "\n";
std::cout << "Mutable lambda (1, 2, 3): "
<< incrementer() << ", " << incrementer() << ", " << incrementer() << "\n";
std::cout << "Recursive lambda (5!): " << factorial(factorial, 5) << "\n";
std::cout << "Original counter (no debe cambiar): " << internal_counter << "\n";
process_async_safe([](int val, int m) {
std::cout << "Async safe (val: " << val << ", mult: " << m << ")\n";
});
}
};
int main() {
DataProcessor processor;
processor.demonstrate();
return 0;
}
Desglose del concepto
En el código anterior, la función get_type_checker devuelve una lambda que utiliza []<typename T>(T val). A diferencia de [](auto val), aquí el compilador instancia una versión de la lambda para cada tipo T de forma explícita, permitiéndonos usar std::decay_t<T> para extraer el tipo base y realizar comprobaciones de tipos en tiempo de compilación.
En process_async_safe, hemos utilizado [*this]. Esto es fundamental para la robustez: en lugar de capturar la dirección de DataProcessor, la lambda crea una copia del objeto. Si esta lambda se enviara a un hilo de fondo y processor fuera destruido antes de ejecutarse, la copia dentro de la lambda seguiría siendo válida, evitando un acceso a memoria ilegal.
La lambda incrementer usa la cláusula mutable. Sin ella, la línea ++internal_counter causaría un error de compilación, ya que internal_counter es un miembro de la clase anónima generada por la lambda, y el método operator() es const por defecto. Noten que internal_counter en demonstrate() permanece en 0 después de las llamadas; esto confirma que estamos modificando una copia local almacenada en el objeto closure.
Para la recursión, el patrón auto factorial = [](auto self, int n) ... es la forma más eficiente de implementar recursividad sin recurrir a std::function. Al pasar la lambda como primer argumento (self), permitimos que la función se llame a sí misma mediante una llamada directa, lo que permite al compilador realizar el inlining de manera mucho más agresiva que con el puntero de función indirecto que requiere std::function.
El error frecuente
Un error clásico en sistemas de alta concurrencia es capturar [this] en una lambda que se despacha a un hilo o a un pool de tareas sin asegurar la vida del objeto.
struct BadTask {
int data = 42;
void run_dangerous() {
// [this] captura el puntero. Si el hilo tarda más que el objeto, crash.
auto task = [this]() {
std::cout << data << std::endl;
};
std::thread(std::move(task)).detach();
} // Aquí 'this' muere, pero el hilo sigue vivo.
};
Si ejecutas esto, es probable que obtengas un error de segmentación o, lo que es peor, un comportamiento indefinido (UB) donde se imprime basura de la pila. Un análisis con AddressSanitizer (-fsanitize=address) detectaría esto inmediatamente como un use-after-free. Para solucionarlo, usa [*this] para capturar una copia o std::shared_from_this si necesitas compartir la propiedad del objeto.
N° 136