Solución:
El problema
C ++ incluye funciones genéricas útiles como std::for_each
y std::transform
, que puede ser muy útil. Desafortunadamente, también pueden ser bastante engorrosos de usar, especialmente si el functor que le gustaría aplicar es exclusivo de 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 solo usa f
una vez y en ese lugar específico parece exagerado escribir una clase completa solo para hacer algo trivial y único.
En C ++ 03, es posible que tenga la tentación de 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 se puede pasar a una función de plantilla en C ++ 03.
La nueva solucion
C ++ 11 introduce lambdas que le permiten escribir un functor anónimo en línea para reemplazar el struct 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 simplemente azúcar sintáctico para functores anónimos.
Tipos de devolución
En casos simples, el tipo de retorno de la lambda se deduce por usted, 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 comience a escribir lambdas más complejas, encontrará rápidamente casos en los que el compilador no puede deducir el tipo de retorno, 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, puede especificar explícitamente un tipo de retorno para una función lambda, usando -> 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;
}
});
}
“Capturando” variables
Hasta ahora no hemos usado nada más que lo que se pasó al lambda dentro de él, pero también podemos usar otras variables, dentro del lambda. Si desea acceder a otras variables, puede 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;
}
});
}
Puede capturar tanto por referencia como por valor, que puede especificar utilizando &
y =
respectivamente:
-
[&epsilon]
capturar por referencia -
[&]
captura todas las variables utilizadas en la lambda por referencia -
[=]
captura todas las variables utilizadas en la lambda por valor -
[&, epsilon]
captura variables como con [&], pero épsilon por valor -
[=, &epsilon]
captura variables como con [=], pero épsilon por referencia
El generado operator()
es const
por defecto, con la implicación de que las capturas serán const
cuando acceda a ellos de forma predeterminada. Esto tiene el efecto de que cada llamada con la misma entrada produciría el mismo resultado, sin embargo, puede marcar la lambda como mutable
para solicitar que el operator()
que se produce no es const
.
¿Qué es una función lambda?
El concepto de C ++ de una función lambda se origina 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 no vale la pena nombrarlos.
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
La lista de captura define qué desde el exterior de la lambda debería estar disponible dentro del cuerpo de la función y cómo. Puede ser:
- un valor: [x]
- una referencia [&x]
- cualquier variable actualmente en el alcance por referencia [&]
- igual que 3, pero por valor [=]
Puede mezclar cualquiera de los anteriores en una lista separada por comas [x, &y]
.
La lista de argumentos
La lista de argumentos es la misma que en cualquier otra función de C ++.
El cuerpo funcional
El código que se ejecutará cuando se llame realmente a la lambda.
Deducción por tipo de devolución
Si una lambda tiene solo una declaración de retorno, el tipo de retorno se puede omitir y tiene el tipo implícito de decltype(return_statement)
.
Mudable
Si una lambda está marcada como mutable (p. Ej. []() mutable { }
) se permite mutar los valores que han sido capturados por valor.
Casos de uso
La biblioteca definida por el estándar ISO se beneficia en gran medida de las lambdas y aumenta la usabilidad varias barras, ya que ahora los usuarios no tienen que abarrotar su código con pequeños functores en algún ámbito accesible.
C ++ 14
En C ++ 14 lambdas se han ampliado con diversas propuestas.
Capturas Lambda inicializadas
Ahora se puede inicializar un elemento de la lista de captura con =
. Esto permite renombrar variables y capturar moviendo. Un ejemplo tomado del estándar:
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;};
Lambdas genéricas
Lambdas ahora puede ser genérico (auto
sería equivalente a T
aquí si
T
eran un argumento de plantilla de tipo en algún lugar del alcance circundante):
auto lambda = [](auto x, auto y) {return x + y;};
Deducción mejorada del tipo de devolución
C ++ 14 permite tipos de retorno deducidos para cada función y no lo restringe a funciones de la forma return expression;
. Esto también se extiende a lambdas.
Las expresiones lambda se utilizan normalmente para encapsular algoritmos para que puedan pasarse a otra función. Sin embargo, es posible ejecutar una lambda inmediatamente después de la definición:
[&](){ ...your code... }(); // immediately executed lambda expression
es funcionalmente equivalente a
{ ...your code... } // simple code block
Esto hace expresiones lambda una poderosa herramienta para refactorizar funciones complejas. Empiece por envolver una sección de código en una función lambda como se muestra arriba. El proceso de parametrización explícita se puede realizar gradualmente con pruebas intermedias después de cada paso. Una vez que tenga el bloque de código completamente parametrizado (como lo demuestra la eliminación del &
), puede mover el código a una ubicación externa y convertirlo en una función normal.
Del mismo modo, puede 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 particionar la lógica de su programa, incluso puede resultarle ú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 le permiten crear nombres funciones anidadas, que puede ser una forma conveniente de evitar la lógica duplicada. El uso de lambdas con nombre también tiende a ser un poco más fácil para los ojos (en comparación con las lambdas en línea anónimas) 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 la generación de perfiles posterior revela una sobrecarga de inicialización significativa para el objeto de función, puede optar por reescribir esto como una función normal.