¿Qué es una expresión lambda en C++11? ¿Cuándo utilizaría una? ¿Qué clase de problema resuelven que no era posible antes de su introducción?
Unos cuantos ejemplos y casos de uso serían útiles.
C++ incluye útiles funciones genéricas como std::for_each
y std::transform
, que pueden ser muy útiles. Desgraciadamente, también pueden ser bastante engorrosas de usar, sobre todo si el functor que quieres aplicar es único para la función en particular.
#include <algorithm>
#include <vector>
namespace {
struct f {
void operator()(int) {
// do something
}
};
}
void func(std::vector<int>& v) {
f f;
std::for_each(v.begin(), v.end(), f);
}
Si sólo se utiliza f
una vez y en ese lugar específico parece exagerado estar escribiendo una clase entera sólo para hacer algo trivial y único.
En C++03 podrías estar tentado a escribir algo como lo siguiente, para mantener el functor local:
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
sin embargo esto no está permitido, f
no puede ser pasado a una función template en C++03.
C++11 introduce lambdas que permiten escribir un functor anónimo en línea para reemplazar la estructura f
. Para pequeños ejemplos simples esto puede ser más limpio de leer (mantiene todo en un solo lugar) y potencialmente más simple de mantener, por ejemplo en la forma más simple:
void func3(std::vector<int>& v) {
std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}
Las funciones lambda son sólo azúcar sintáctico para los funtores anónimos.
En casos sencillos, el tipo de retorno de la lambda se deduce por ti, por ejemplo
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}
Sin embargo, cuando empieces a escribir lambdas más complejas te encontrarás rápidamente con casos en los que el tipo de retorno no puede ser deducido por el compilador, por ejemplo
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
Para resolver esto se permite especificar explícitamente un tipo de retorno para una función lambda, utilizando -> T
:
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) -> double {
if (d < 0.0001) {
return 0;
} else {
return d;
}
});
}
Hasta ahora no hemos utilizado nada más que lo que se ha pasado a la lambda dentro de ella, pero también podemos utilizar otras variables, dentro de la lambda. Si quieres acceder a otras variables puedes utilizar la cláusula de captura (la []
de la expresión), que hasta ahora no se ha utilizado en estos ejemplos, por ejemplo
void func5(std::vector<double>& v, const double& epsilon) {
std::transform(v.begin(), v.end(), v.begin(),
[epsilon](double d) -> double {
if (d < epsilon) {
return 0;
} else {
return d;
}
});
}
Puedes capturar tanto por referencia como por valor, que puedes especificar usando &
y =
respectivamente:
[&epsilon]
captura por referencia[&]
captura por referencia todas las variables utilizadas en la lambda[=]
captura todas las variables utilizadas en la lambda por valor[&, epsilon]
captura las variables como con [&], pero epsilon por valor[=, &epsilon]
captura variables como con [=], pero epsilon por referenciaEl operador()
generado es const
por defecto, con la implicación de que las capturas serán const
cuando se acceda a ellas por defecto. Esto tiene el efecto de que cada llamada con la misma entrada producirá el mismo resultado, sin embargo puedes marcar la lambda como mutable
para solicitar que el operator()
que se produzca no sea const
.
El concepto de función lambda en C++ tiene su origen en el cálculo lambda y la programación funcional. Una lambda es una función sin nombre que es útil (en la programación real, no en la teoría) para fragmentos cortos de código que son imposibles de reutilizar y que no vale la pena nombrar.
En C++ una función lambda se define así
[]() { } // barebone lambda
o en todo su esplendor
[]() mutable -> T { } // T is the return type, still lacking throw()
[]
es la lista de captura, ()
la lista de argumentos y {}
el cuerpo de la función.
La lista de captura define lo que desde el exterior de la lambda debe estar disponible dentro del cuerpo de la función y cómo. Puede ser:
Puedes mezclar cualquiera de los anteriores en una lista separada por comas [x, &y]
.
La lista de argumentos es la misma que en cualquier otra función de C++.
El código que se ejecutará cuando se llame a la lambda.
Si una lambda sólo tiene una sentencia de retorno, el tipo de retorno puede omitirse y tiene el tipo implícito de decltype(return_statement)
.
Si una lambda está marcada como mutable (por ejemplo, []() mutable { }
) se permite mutar los valores que han sido capturados por valor.
La librería definida por el estándar ISO se beneficia mucho de las lambdas y eleva la usabilidad varios peldaños ya que ahora los usuarios no tienen que abarrotar su código con pequeños funtores en algún ámbito accesible.
En C++14 las lambdas han sido ampliadas por varias propuestas.
Un elemento de la lista de captura ahora puede ser inicializado con =
. Esto permite renombrar las variables y capturar por desplazamiento. Un ejemplo tomado de la norma:
int x = 4;
auto y = [&r = x, x = x+1]()->int {
r += 2;
return x+2;
}(); // Updates ::x to 6, and initializes y to 7.
y uno tomado de Wikipedia que muestra cómo capturar con std::move
:
auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};
Las lambdas ahora pueden ser genéricas (auto
sería equivalente a T
aquí si
T` fuera un argumento de plantilla de tipo en algún lugar del ámbito circundante):
auto lambda = [](auto x, auto y) {return x + y;};
C++14 permite deducir los tipos de retorno para cada función y no lo restringe a funciones de la forma expresión de retorno;
. Esto también se extiende a las lambdas.
Las expresiones lambda se utilizan normalmente para encapsular algoritmos de manera que puedan ser pasados a otra función. Sin embargo, es posible ejecutar una lambda inmediatamente después de su definición:
[&](){ ...your code... }(); // immediately executed lambda expression
es funcionalmente equivalente a
{ ...your code... } // simple code block
Esto hace de las expresiones lambda una poderosa herramienta para refactorizar funciones complejas. Se empieza por envolver una sección de código en una función lambda como se muestra arriba. El proceso de parametrización explícita puede entonces realizarse gradualmente con pruebas intermedias después de cada paso. Una vez que tengas el bloque de código completamente parametrizado (como se demuestra con la eliminación de la &
), puedes mover el código a una ubicación externa y convertirlo en una función normal.
Del mismo modo, puedes utilizar expresiones lambda para inicializar variables basadas en el resultado de un algoritmo...
int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!
Como una forma de dividir la lógica de tu programa, puedes incluso encontrar útil pasar una expresión lambda como argumento a otra expresión lambda...
[&]( std::function<void()> algorithm ) // wrapper section
{
...your wrapper code...
algorithm();
...your wrapper code...
}
([&]() // algorithm section
{
...your algorithm code...
});
Las expresiones lambda también te permiten crear funciones anidadas con nombre, lo que puede ser una forma conveniente de evitar la duplicación de la lógica. Usar lambdas con nombre también tiende a ser un poco más fácil para los ojos (en comparación con las lambdas anónimas en línea) cuando se pasa una función no trivial como parámetro a otra función. *Nota: no olvide el punto y coma después de la llave de cierre.
auto algorithm = [&]( double x, double m, double b ) -> double
{
return m*x+b;
};
int a=algorithm(1,2,3), b=algorithm(4,5,6);
Si el perfil posterior revela una sobrecarga de inicialización significativa para el objeto de la función, puede optar por reescribir esto como una función normal.