Menu

Lambdas en C++: funciones anónimas explicadas con ejemplos

Escribe pequeñas funciones en línea sobre la marcha con las lambdas de C++: la sintaxis, cómo funcionan las capturas, cuándo usar mutable y la trampa de la captura colgante que pilla a todo el mundo.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Funciones que escribes donde las usas

En la página anterior viste cómo la sobrecarga permite que varias funciones compartan un nombre. Pero a veces no quieres una función con nombre en absoluto: necesitas una pequeña pieza de lógica una sola vez, justo donde la usas, y ponerle nombre solo añadiría ruido. Eso es lo que es una lambda: una función anónima que puedes definir en línea.

Una lambda tiene una forma característica de cuatro partes:

[capture](parameters) -> return_type { body }

El [] es la señal de que estás ante una lambda. El tipo de retorno es opcional; normalmente el compilador lo deduce. Esta es la más sencilla posible:

greet es simplemente una variable (su tipo es indescriptible, así que la guardas con auto) que puedes llamar con (). Las lambdas con parámetros funcionan exactamente igual que las funciones normales:

Capturas: alcanzar el ámbito circundante

La parte que hace que las lambdas sean algo más que funciones sin nombre es la lista de captura: el []. Permite que la lambda use variables del ámbito donde se definió, no solo sus propios parámetros.

Captura por valor con [x]: la lambda obtiene su propia copia, congelada en el momento en que se crea la lambda.

Fíjate en que scale(5) imprimió 50, usando el valor de factor igual a 10 que existía cuando se creó la lambda. La captura por valor hace una instantánea.

Captura por referencia con [&x]: la lambda se refiere a la variable original, ve los cambios posteriores y puede modificarla.

También puedes capturar todo lo que use la lambda con [=] (todo por valor) o [&] (todo por referencia). Son cómodos, pero ser explícito -[total] o [&total]- documenta exactamente qué toca la lambda y es más fácil de razonar.

La trampa de la referencia colgante

Capturar por referencia es tan poderoso como peligroso. La referencia solo es válida mientras la variable original siga viva. Si la lambda vive más que lo que capturó, obtienes una referencia colgante y comportamiento indefinido: el programa podría fallar, podría imprimir basura, podría parecer que funciona por accidente.

Este es el error clásico: devolver una lambda que captura una variable local por referencia.

auto makeCounter() {
    int count = 0;
    return [&count]() { return ++count; };  // ERROR: count muere aquí
}
// La lambda devuelta ahora referencia memoria destruida.

Cuando makeCounter retorna, su local count se destruye, pero la lambda todavía mantiene una referencia a él. Llamar a la lambda devuelta toca memoria muerta. La solución es capturar por valor para que la lambda posea su propio estado:

Regla práctica: captura por referencia solo cuando la lambda se usa inmediatamente y de forma local (como con los algoritmos de más abajo). En el momento en que una lambda se almacena, se devuelve o se ejecuta más tarde, prefiere capturar por valor.

mutable y tipos de retorno

¿Te fijaste en el mutable de ese último ejemplo? Por defecto, una captura por valor es const dentro de la lambda: puedes leer la copia pero no cambiarla. Añadir mutable permite que la lambda modifique sus propias copias entre llamadas.

mutable solo afecta a la copia privada de la lambda: el seen exterior queda intacto, que es precisamente el sentido de capturar por valor.

La mayoría de las veces el compilador deduce el tipo de retorno sin problema. Solo necesitas especificarlo con -> cuando hay ambigüedad, como una lambda que podría devolver distintos tipos en distintas ramas:

// Sin -> el compilador no puede decidir entre int y double
auto half = [](int n) -> double {
    if (n % 2 == 0) return n / 2;   // int
    return n / 2.0;                 // double
};

Lambdas y algoritmos: la verdadera recompensa

La razón por la que se añadieron las lambdas a C++ es alimentar con pequeñas piezas de lógica a los algoritmos de la biblioteca estándar. Antes de las lambdas tenías que escribir una función con nombre aparte o un torpe objeto función lejos de donde se usaba. Ahora la lógica vive justo en el punto de la llamada.

El ejemplo más común es un orden de clasificación personalizado:

Las capturas brillan aquí porque la lambda puede traer un valor para filtrar o contar contra él. Esto cuenta cuántos números superan un umbral elegido por el usuario:

Como estas lambdas se usan inmediatamente y no viven más que la función circundante, capturar por referencia ([&passMark]) también sería seguro aquí, pero por valor es igual de claro y nunca queda colgante.

Siguiente: Punteros

Las lambdas plantearon en voz baja una pregunta más profunda: cuando capturas [&x], la lambda se aferra a la ubicación de x, y esa ubicación solo es válida mientras x viva. Esa idea -un valor que se refiere a dónde vive algo en memoria, y qué pasa cuando aquello a lo que apunta desaparece- es exactamente de lo que trata la siguiente página. Nos enfrentaremos cara a cara con los punteros: cómo tomar una dirección, cómo seguirla y cómo el mismo problema de colgar que acabas de ver aparece a lo largo de todo C++.

Preguntas frecuentes

¿Qué es una lambda en C++?

Una lambda es una función anónima que puedes escribir en línea, justo donde la usas. La sintaxis es [capturas](parámetros){ cuerpo }. Es perfecta para operaciones cortas y puntuales -como el comparador que le pasas a std::sort- sin tener que declarar una función con nombre aparte en otro lugar.

¿Cuál es la diferencia entre capturar por valor y por referencia en una lambda de C++?

[x] captura una copia de x congelada en el momento en que se crea la lambda. [&x] captura una referencia al x original, así que la lambda ve los cambios posteriores y puede modificarlo. Usa [&] solo mientras las variables capturadas tengan garantizado que vivirán más que la lambda, o tendrás una referencia colgante.

¿Por qué mi lambda de C++ dice que no puede modificar una variable capturada?

Las capturas por valor son const dentro de la lambda por defecto. Añade la palabra clave mutable -[x]() mutable { x++; }- para permitir que la lambda cambie su propia copia. Ten en cuenta que esto solo cambia la copia de la lambda, no la variable original de fuera.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR