functions funciones expresiones closure c++ lambda c++11 c++-faq

funciones - lambda function c++ 11



¿Qué es una expresión lambda en C++ 11? (8)

¿Qué es una expresión lambda en C ++ 11? ¿Cuándo usaría uno? ¿Qué clase de problema resuelven que no fue posible antes de su introducción?

Unos pocos ejemplos, y casos de uso serían útiles.


¿Qué es una función lambda?

El concepto C ++ de una función lambda se origina en el cálculo lambda y la programación funcional. Un lambda es una función sin nombre que es útil (en programación real, no en 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é elementos externos deben estar disponibles dentro del cuerpo de la función y cómo. Puede ser:

  1. un valor: [x]
  2. una referencia [& x]
  3. cualquier variable actualmente en alcance por referencia [&]
  4. 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 de la funcion

El código que se ejecutará cuando se llame la lambda.

Tipo de devolución deducción

Si un 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 (por ejemplo, []() mutable { } ), se permite mutar los valores capturados por valor.

Casos de uso

La biblioteca definida por la norma ISO se beneficia en gran medida de las lambdas y aumenta la usabilidad de varias barras, ya que ahora los usuarios no tienen que saturar su código con pequeños funtores en algún ámbito accesible.

C ++ 14

En C ++ 14 las lambdas se han extendido por varias propuestas.

Capturas Lambda Inicializadas

Un elemento de la lista de captura ahora se puede inicializar con = . Esto permite renombrar variables y capturar moviendo. 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 una tomada 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érico

Lambdas ahora puede ser genérico ( auto sería equivalente a T aquí si T fuera un argumento de tipo de plantilla en algún lugar del ámbito que lo rodea):

auto lambda = [](auto x, auto y) {return x + y;};

Deducción del tipo de retorno mejorado

C ++ 14 permite tipos de devolución deducidos para cada función y no la restringe a funciones de la return expression; formulario return expression; . Esto también se extiende a las lambdas.


El problema

C ++ incluye funciones genéricas útiles como std::for_each y std::transform , que pueden ser muy útiles. Desafortunadamente, también pueden ser bastante incómodos de usar, especialmente si el functor que desea 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 usas f una vez y en ese lugar específico, parece excesivo escribir una clase entera solo para hacer algo trivial y único.

En C ++ 03, puede tener 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 template en C ++ 03.

La nueva solucion

C ++ 11 introduce lambdas que le permiten escribir un funtor anónimo en línea para reemplazar la struct f . Para pequeños ejemplos simples, esto puede ser más fácil de leer (mantiene todo en un solo lugar) y potencialmente más sencillo 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 solo azúcar sintáctica para funtores anónimos.

Tipos de retorno

En casos simples, el tipo de devolución 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 comienza a escribir lambdas más complejas, encontrará rápidamente casos en los que el compilador no puede deducir el tipo de devolución, 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 le permite 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ó a la lambda dentro de ella, pero también podemos usar otras variables, dentro de la 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 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 operator() generado operator() es const de forma predeterminada, con la implicación de que las capturas serán const cuando se accede 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 sea const .


Bueno, un uso práctico que he descubierto 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 casos de bsize . Por supuesto, puede 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 con este requisito y lo uso para ese caso.


Las expresiones Lambda se utilizan normalmente para encapsular algoritmos de modo 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 . Empiece 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 tenga el bloque de código totalmente parametrizado (como lo demuestra la eliminación de la & ), 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 en función del 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 tu programa , incluso puedes encontrar útil pasar una expresión lambda como un 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, lo 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 el perfil posterior revela una sobrecarga de inicialización significativa para el objeto de función, puede optar por volver a escribir esto como una función normal.



Una de las mejores explicaciones de la lambda expression es la del autor de C ++ Bjarne Stroustrup en su libro ***The C++ Programming Language*** capítulo 11 ( ISBN-13: 978-0321563842 ):

What is a lambda expression?

Una expresión lambda , a veces también conocida como una función lambda o (estrictamente hablando de manera incorrecta, pero coloquial) como una lambda , es una notación simplificada para definir y utilizar un objeto de función anónimo . En lugar de definir una clase nombrada 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 un 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 ellos, 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. Como triste por Stroustup:

formas efectivas de optimización

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 vía 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 necesitas, puedes nombrar la 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 supongamos 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á siguiente

0

1

0

1

0

1

0

1

0

1

0 sortedx - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[] - esta es la lista de captura o lambda introducer : si las lambdas no requieren 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 utilizar 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 utilizar todos los nombres locales. Se accede a todas las variables locales por referencia.

[=] : captura implícitamente por valor. Se pueden utilizar 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 los nombres seguidos de ... como elementos.

[&, lista de captura] : 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 ir 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 listados deben ir precedidos por &. 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

Formato de Lambda expression

Referencias adicionales:


Una función lambda es una función anónima que creas en línea. Puede capturar variables como algunos lo han explicado (por ejemplo, http://www.stroustrup.com/C++11FAQ.html#lambda ) pero hay 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 lugar para usarla como la que se pasó 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''); }); }

Por 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 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''); }); }


Respuestas

P: ¿Qué es una expresión lambda en C ++ 11?

R: Bajo el capó, es el objeto de una clase autogenerada con const de operador de sobrecarga () . Tal objeto se llama cierre y creado por el compilador. Este concepto de "cierre" se acerca al concepto de enlace de C ++ 11. Pero las lambdas suelen generar mejor código. Y las llamadas a través de los cierres permiten la inclusión completa.

P: ¿Cuándo usaría uno?

R: Para definir "lógica simple y pequeña" y pedir al compilador que realice una generación a partir de la pregunta anterior. Le das a un compilador algunas expresiones que quieres que estén dentro de operator (). Todo lo demás compilador te generará.

P: ¿Qué clase de problema resuelven que no fue posible antes de su introducción?

R: Es una especie de azúcar de sintaxis como operadores que sobrecargan en lugar de funciones para operaciones de adición, sobraactividad personalizada ... ¡Pero guarda más líneas de código innecesario para ajustar 1-3 líneas de lógica real a algunas clases, y etc.! Algunos ingenieros piensan que si el número de líneas es más pequeño, 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 cubiertas por pregunta. Ignora esta sección si no estás interesado.

1. Valores capturados. Lo que puedas capturar

1.1. Puede hacer referencia a una variable con duración de almacenamiento estático en lambdas. Todos ellos son capturados.

1.2. Puede utilizar lambda para los valores de captura "por valor". En tal caso, las variables capturadas se copiarán al objeto de función (cierre).

[captureVar1,captureVar2](int arg1){}

1.3. Se puede capturar como referencia. & - en este contexto significa referencia, no punteros.

[&captureVar1,&captureVar2](int arg1){}

1.4. Existe notación para capturar todos los vars 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 una notación para capturar todas las vars no estáticas por valor, o por referencia y especificar algo. Más. Ejemplos: Capture todas las vars no estáticas por valor, pero por captura de referencia Param2

[=,&Param2](int arg1){}

Capture todos los vars no estáticos por referencia, pero por captura de valor Param2

[&,Param2](int arg1){}

2. Devolución del tipo de devolució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, entonces el tipo de retorno debe especificarse a través del 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 vars locales, no la variable miembro del objeto.

4. Revisiones

4.1 !! Lambda no es un puntero de función y no es una función anónima, pero las lambdas sin captura pueden convertirse implícitamente en un puntero de función.

PD

  1. 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 de Lambda, p.88

  2. En C ++ 14 se ha agregado la característica adicional que se ha denominado como "captura de inicio". Permite realizar declaración arbitraria de miembros de datos de cierre:

    auto toFloat = [](int value) { return float(value);}; auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};