¿Qué es una expresión lambda en C ++ 11? ¿Cuándo usaría uno? ¿Qué clase de problema resuelven que no era posible antes de su introducción?
Algunos ejemplos y casos de uso serían útiles.
¿Qué es una expresión lambda en C ++ 11? ¿Cuándo usaría uno? ¿Qué clase de problema resuelven que no era posible antes de su introducción?
Algunos ejemplos y casos de uso serían útiles.
Respuestas:
C ++ incluye funciones genéricas útiles como std::for_each
y std::transform
, que pueden ser muy útiles. Desafortunadamente, también pueden ser bastante engorrosos de usar, especialmente si el functor que desea aplicar es único para la función 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 excesivo escribir toda una clase solo para hacer algo trivial y único.
En C ++ 03 puede 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.
C ++ 11 presenta 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 de Lambda son simplemente azúcar sintáctica para functores anónimos.
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, 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 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 usar 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 usando &
y =
respectivamente:
[&epsilon]
captura por referencia[&]
captura todas las variables utilizadas en el lambda por referencia[=]
captura todas las variables utilizadas en lambda por valor[&, epsilon]
captura variables como con [&], pero epsilon por valor[=, &epsilon]
captura variables como con [=], pero epsilon por referenciaEl generado operator()
es const
por defecto, con la implicación de que las capturas serán const
cuando accedas a ellas por defecto. Esto tiene el efecto de que cada llamada con la misma entrada produciría el mismo resultado, sin embargo, puede marcar el lambdamutable
para solicitar que lo operator()
que se produce no sea const
.
const
siempre ...
()
, se pasa como un lambda de argumento cero, pero debido a () const
que no coincide con el lambda, busca una conversión de tipo que lo permita, que incluye la conversión implícita -to-function-pointer, y luego llama a eso! ¡Furtivo!
std::function<double(int, bool)> f = [](int a, bool b) -> double { ... };
Pero generalmente, dejamos que el compilador deduzca el tipo: auto f = [](int a, bool b) -> double { ... };
(y no olvide #include <functional>
)
return d < 0.00001 ? 0 : d;
se garantiza que devolverá el doble, cuando uno de los operandos es una constante entera (es debido a una regla de promoción implícita del operador?: Donde el segundo y tercer operando se equilibran entre sí a través de la aritmética habitual conversiones sin importar cuál sea elegido). Cambiar a 0.0 : d
tal vez haría que el ejemplo sea más fácil de entender.
El concepto 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 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 qué desde el exterior del lambda debería estar disponible dentro del cuerpo de la función y cómo. Puede ser:
Puede 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 realmente al lambda.
Si una lambda tiene una sola declaración 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 le permite mutar los valores que han sido capturados por valor.
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 saturar su código con pequeños functors en un alcance accesible.
En C ++ 14 lambdas se han ampliado con varias propuestas.
Ahora se puede inicializar un elemento de la lista de captura con =
. Esto permite cambiar el nombre de las 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 ahora puede ser genérico ( auto
sería equivalente a T
aquí si
T
fuera un argumento de plantilla de tipo en algún lugar del alcance circundante):
auto lambda = [](auto x, auto y) {return x + y;};
C ++ 14 permite tipos de retorno deducidos para cada función y no lo restringe a funciones del formulario return expression;
. Esto también se extiende a las lambdas.
r = &x; r += 2;
pero esto sucede con el valor original de 4.
Las expresiones lambda se usan típicamente para encapsular algoritmos para que puedan pasarse a otra función. Sin embargo, es posible ejecutar un 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 que las expresiones lambda sean una herramienta poderosa para refactorizar funciones complejas . Comienza envolviendo 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 tiene 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 usar 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 funciones anidadas con nombre , que pueden 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 la vista (en comparación con lambdas en línea anónimas) al pasar una función no trivial como parámetro a otra función. Nota: no olvide el punto y coma después del cierre de llaves.
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 creació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.
if
declaraciones: if ([i]{ for (char j : i) if (!isspace(j)) return false ; return true ; }()) // i is all whitespace
suponiendo que i
sea unstd::string
[](){}();
.
(lambda: None)()
sintaxis de Python es mucho más legible.
main() {{{{((([](){{}}())));}}}}
Respuestas
P: ¿Qué es una expresión lambda en C ++ 11?
R: Bajo el capó, es el objeto de una clase autogenerada con sobrecarga operator () const . Dicho objeto se llama cierre y lo crea el compilador. Este concepto de 'cierre' está cerca del concepto de enlace de C ++ 11. Pero las lambdas generalmente generan un mejor código. Y las llamadas a través de cierres permiten una alineación completa.
P: ¿Cuándo usaría uno?
R: Para definir "lógica simple y pequeña" y pedirle al compilador que realice la generación de la pregunta anterior. Le da al compilador algunas expresiones que desea que estén dentro del operador (). El resto del compilador de cosas te generará.
P: ¿Qué clase de problema resuelven que no era posible antes de su introducción?
R: Es una especie de azúcar de sintaxis, como la sobrecarga de operadores en lugar de funciones para operaciones personalizadas de suma, resta ... ¡Pero guarda más líneas de código innecesario para ajustar 1-3 líneas de lógica real a algunas clases, etc.! Algunos ingenieros piensan que si el número de líneas es menor, entonces hay menos posibilidades de cometer errores (también lo creo)
Ejemplo de uso
auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);
Extras sobre lambdas, no cubiertos por la pregunta. Ignora esta sección si no te interesa
1. Valores capturados. Lo que puedes capturar
1.1. Puede hacer referencia a una variable con duración de almacenamiento estático en lambdas. Todos son capturados.
1.2. Puede usar lambda para capturar valores "por valor". En tal caso, los vars capturados se copiarán al objeto de función (cierre).
[captureVar1,captureVar2](int arg1){}
1.3. Puede capturar ser referencia. & - en este contexto, significa referencia, no punteros.
[&captureVar1,&captureVar2](int arg1){}
1.4. Existe notación para capturar todos los valores no estáticos por valor o por referencia
[=](int arg1){} // capture all not-static vars by value
[&](int arg1){} // capture all not-static vars by reference
1.5. Existe notación para capturar todos los vars no estáticos por valor, o por referencia y especificar smth. más. Ejemplos: Capture todos los valores no estáticos por valor, sino por captura de referencia Param2
[=,&Param2](int arg1){}
Capture todos los vars no estáticos por referencia, pero mediante captura de valor Param2
[&,Param2](int arg1){}
2. Tipo de devolución deducción
2.1. El tipo de retorno Lambda se puede deducir si lambda es una expresión. O puede especificarlo explícitamente.
[=](int arg1)->trailing_return_type{return trailing_return_type();}
Si lambda tiene más de una expresión, el tipo de retorno debe especificarse mediante el tipo de retorno final. Además, se puede aplicar una sintaxis similar a las funciones automáticas y las funciones miembro
3. Valores capturados. Lo que no puedes capturar
3.1. Puede capturar solo variables locales, no variables de miembro del objeto.
4. Conversiones
4.1 !! Lambda no es un puntero de función y no es una función anónima, pero las lambdas sin captura se pueden convertir implícitamente en un puntero de función.
PD
Puede encontrar más información sobre la gramática lambda en el Borrador de trabajo para el lenguaje de programación C ++ # 337, 2012-01-16, 5.1.2. Expresiones Lambda, p.88
En C ++ 14 se ha agregado la característica adicional que se ha denominado como "captura inicial". Permite realizar declaraciones de miembros de datos de cierre arbitrariamente:
auto toFloat = [](int value) { return float(value);};
auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
[&,=Param2](int arg1){}
no parece ser una sintaxis válida. La forma correcta sería[&,Param2](int arg1){}
Una función lambda es una función anónima que crea en línea. Puede capturar variables como algunos han explicado (por ejemplo, http://www.stroustrup.com/C++11FAQ.html#lambda ) pero existen algunas limitaciones. Por ejemplo, si hay una interfaz de devolución de llamada como esta,
void apply(void (*f)(int)) {
f(10);
f(20);
f(30);
}
puede escribir una función en el acto para usarla como la que se pasa para aplicar a continuación:
int col=0;
void output() {
apply([](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
Pero no puedes hacer esto:
void output(int n) {
int col=0;
apply([&col,n](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
debido a limitaciones en el estándar C ++ 11. Si desea utilizar capturas, debe confiar en la biblioteca y
#include <functional>
(o alguna otra biblioteca STL como el algoritmo para obtenerlo indirectamente) y luego trabajar con std :: function en lugar de pasar funciones normales como parámetros como este:
#include <functional>
void apply(std::function<void(int)> f) {
f(10);
f(20);
f(30);
}
void output(int width) {
int col;
apply([width,&col](int data) {
cout << data << ((++col % width) ? ' ' : '\n');
});
}
apply
fuera una plantilla que aceptara un functor, funcionaría
Una de las mejores explicaciones lambda expression
es la del autor de C ++ Bjarne Stroustrup en su ***The C++ Programming Language***
capítulo 11 del libro ( ISBN-13: 978-0321563842 ):
What is a lambda expression?
Una expresión lambda , a veces también denominada función lambda o (estrictamente hablando incorrectamente, pero coloquialmente) como lambda , es una notación simplificada para definir y usar un objeto de función anónimo . En lugar de definir una clase con nombre con un operador (), luego hacer un objeto de esa clase y finalmente invocarlo, podemos usar una taquigrafía.
When would I use one?
Esto es particularmente útil cuando queremos pasar una operación como argumento a un algoritmo. En el contexto de las interfaces gráficas de usuario (y en otros lugares), tales operaciones a menudo se denominan devoluciones de llamada .
What class of problem do they solve that wasn't possible prior to their introduction?
Aquí supongo que cada acción realizada con la expresión lambda se puede resolver sin ellas, pero con mucho más código y una complejidad mucho mayor. Expresión Lambda: esta es la forma de optimización de su código y una forma de hacerlo más atractivo. Tan triste por Stroustup:
formas efectivas de optimizar
Some examples
a través de la expresión lambda
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for_each(begin(v),end(v),
[&os,m](int x) {
if (x%m==0) os << x << '\n';
});
}
o a través de la función
class Modulo_print {
ostream& os; // members to hold the capture list int m;
public:
Modulo_print(ostream& s, int mm) :os(s), m(mm) {}
void operator()(int x) const
{
if (x%m==0) os << x << '\n';
}
};
o incluso
void print_modulo(const vector<int>& v, ostream& os, int m)
// output v[i] to os if v[i]%m==0
{
class Modulo_print {
ostream& os; // members to hold the capture list
int m;
public:
Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
void operator()(int x) const
{
if (x%m==0) os << x << '\n';
}
};
for_each(begin(v),end(v),Modulo_print{os,m});
}
si lo necesita puede nombrar lambda expression
como a continuación:
void print_modulo(const vector<int>& v, ostream& os, int m)
// output v[i] to os if v[i]%m==0
{
auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
for_each(begin(v),end(v),Modulo_print);
}
O asuma otra muestra simple
void TestFunctions::simpleLambda() {
bool sensitive = true;
std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});
sort(v.begin(),v.end(),
[sensitive](int x, int y) {
printf("\n%i\n", x < y);
return sensitive ? x < y : abs(x) < abs(y);
});
printf("sorted");
for_each(v.begin(), v.end(),
[](int x) {
printf("x - %i;", x);
}
);
}
generará el próximo
0 0
1
0 0
1
0 0
1
0 0
1
0 0
1
0 clasificado x - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;
[]
- esta es una lista de captura o lambda introducer
: si lambdas
no requiere acceso a su entorno local, podemos usarla.
Cita del libro:
El primer carácter de una expresión lambda es siempre [ . Un introductor lambda puede tomar varias formas:
• [] : una lista de captura vacía. Esto implica que no se pueden usar nombres locales del contexto circundante en el cuerpo lambda. Para tales expresiones lambda, los datos se obtienen de argumentos o de variables no locales.
• [&] : captura implícitamente por referencia. Se pueden usar todos los nombres locales. Se accede a todas las variables locales por referencia.
• [=] : captura implícitamente por valor. Se pueden usar todos los nombres locales. Todos los nombres se refieren a copias de las variables locales tomadas en el punto de llamada de la expresión lambda.
• [lista de captura]: captura explícita; la lista de captura es la lista de nombres de variables locales que se capturarán (es decir, se almacenarán en el objeto) por referencia o por valor. Las variables con nombres precedidos por & se capturan por referencia. Otras variables son capturadas por valor. Una lista de captura también puede contener esto y nombres seguidos de ... como elementos.
• [&, capture-list] : captura implícitamente por referencia todas las variables locales con nombres no mencionados en la lista. La lista de captura puede contener esto. Los nombres listados no pueden estar precedidos por &. Las variables nombradas en la lista de captura se capturan por valor.
• [=, lista de captura] : captura implícitamente por valor todas las variables locales con nombres no mencionados en la lista. La lista de captura no puede contener esto. Los nombres enumerados deben ir precedidos de &. Las variables nombradas en la lista de captura se capturan por referencia.
Tenga en cuenta que un nombre local precedido por & siempre se captura por referencia y un nombre local no precedido por & siempre se captura por valor. Solo la captura por referencia permite la modificación de variables en el entorno de llamada.
Additional
Lambda expression
formato
Referencias adicionales:
for (int x : v) { if (x % m == 0) os << x << '\n';}
Bueno, un uso práctico que descubrí es reducir el código de la placa de la caldera. Por ejemplo:
void process_z_vec(vector<int>& vec)
{
auto print_2d = [](const vector<int>& board, int bsize)
{
for(int i = 0; i<bsize; i++)
{
for(int j=0; j<bsize; j++)
{
cout << board[bsize*i+j] << " ";
}
cout << "\n";
}
};
// Do sth with the vec.
print_2d(vec,x_size);
// Do sth else with the vec.
print_2d(vec,y_size);
//...
}
Sin lambda, es posible que deba hacer algo para diferentes bsize
casos. Por supuesto, podría crear una función, pero ¿qué sucede si desea limitar el uso dentro del alcance de la función de usuario del alma? la naturaleza de lambda cumple este requisito y lo uso para ese caso.
Las lambda en c ++ se tratan como "funciones disponibles en el camino". sí, está literalmente en movimiento, usted lo define; úsalo; y cuando el alcance de la función principal termina, la función lambda desaparece.
c ++ lo introdujo en c ++ 11 y todos comenzaron a usarlo como en todos los lugares posibles. el ejemplo y lo que es lambda se puede encontrar aquí https://en.cppreference.com/w/cpp/language/lambda
Describiré cuál no está allí, pero es esencial saberlo para cada programador de C ++
Lambda no está destinado a usarse en todas partes y todas las funciones no se pueden reemplazar con lambda. Tampoco es el más rápido en comparación con la función normal. porque tiene algunos gastos generales que deben ser manejados por lambda.
seguramente ayudará a reducir el número de líneas en algunos casos. se puede usar básicamente para la sección de código, que se llama en la misma función una o más veces y ese fragmento de código no se necesita en ningún otro lugar para que pueda crear una función independiente para él.
A continuación se muestra el ejemplo básico de lambda y lo que sucede en segundo plano.
Codigo de usuario:
int main()
{
// Lambda & auto
int member=10;
auto endGame = [=](int a, int b){ return a+b+member;};
endGame(4,5);
return 0;
}
Cómo se expande la compilación:
int main()
{
int member = 10;
class __lambda_6_18
{
int member;
public:
inline /*constexpr */ int operator()(int a, int b) const
{
return a + b + member;
}
public: __lambda_6_18(int _member)
: member{_member}
{}
};
__lambda_6_18 endGame = __lambda_6_18{member};
endGame.operator()(4, 5);
return 0;
}
como puede ver, qué tipo de sobrecarga agrega cuando lo usa. así que no es buena idea usarlos en todas partes. Se puede utilizar en lugares donde sean aplicables.
Un problema que resuelve: código más simple que lambda para una llamada en el constructor que usa una función de parámetro de salida para inicializar un miembro constante
Puede inicializar un miembro constante de su clase, con una llamada a una función que establece su valor devolviendo su salida como un parámetro de salida.