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


¿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 presentación?

Algunos ejemplos y casos de uso serían útiles.


1195
2017-10-02 14:58


origen


Respuestas:


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, particularmente si el functor le gustaría 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 usas f una vez y en ese lugar específico, parece excesivo escribir una clase completa solo para hacer algo trivial y uno solo.

En C ++ 03 es posible que tengas la tentación de escribir algo como lo siguiente, para mantener el funtor 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 solución

C ++ 11 introduce lambdas le permite escribir un functor anónimo en línea para reemplazar el struct f. Para pequeños ejemplos simples, puede ser más fácil 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 solo azúcar sintáctica para funtores anónimos.

Tipos de devolución

En casos simples, se deduce el tipo de devolución de la lambda, 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 comienzas a escribir lambdas más complejas, encontrarás rápidamente casos en los que el compilador no puede deducir el tipo de retorno, p. ej .:

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

Variables de "captura"

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 (el [] de la expresión), que hasta ahora no se ha usado en estos ejemplos, p. ej .:

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 epsilon 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 accedes a ellos por defecto. Esto tiene el efecto de que cada llamada con la misma entrada produciría el mismo resultado, sin embargo, usted puede marca el lambda como mutable para solicitar que el operator() que se produce no es const.


1206
2017-10-02 15:21



¿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 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

La lista de captura define qué parte del exterior de la lambda debería estar disponible 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 función

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

Devolución de tipo de devolución

Si un lambda tiene solo una declaración de retorno, el tipo de retorno puede omitirse y tiene el tipo implícito de decltype(return_statement).

Mudable

Si una lambda está marcada como mutable (p. []() mutable { }) se permite mutar los valores que se han capturado por valor.

Casos de uso

La biblioteca definida por el estándar ISO se beneficia enormemente de lambdas y aumenta la usabilidad de varias barras ya que ahora los usuarios no tienen que ocupar su código con pequeños funtores en algún ámbito accesible.

C ++ 14

En C ++ 14 lambdas han sido ampliadas por varias propuestas.

Capturas de Lambda inicializadas

Un elemento de la lista de captura ahora se puede inicializar 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érico

Lambdas ahora puede ser genérico (auto sería equivalente a T aquí si T fueron 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 las funciones de la forma return expression;. Esto también se extiende a lambdas.


718
2017-10-02 15:43



Las expresiones Lambda se usan generalmente 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 una poderosa herramienta 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 tenga el bloque de código completamente 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 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 tu programa, incluso podría 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 te 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 el perfilado posterior revela una sobrecarga significativa de inicialización para el objeto de función, puede optar por reescribir esto como una función normal.


151
2018-03-01 08:08



Respuestas

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

A: Debajo del capó, es el objeto de una clase autogenerada con sobrecarga operador () const. Tal objeto se llama cierre y creado por 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 la creación de líneas completas.

P: ¿Cuándo usaría uno?

R: Para definir "lógica simple y pequeña" y preguntar al compilador que realice la generación de la pregunta anterior. Le das al compilador algunas expresiones que quieres que sean dentro del operador (). Todo el compilador de otras cosas te generará.

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

A: Es un tipo de azúcar sintáctica como la sobrecarga de operadores en lugar de funciones para personalizar agregar, someter operaciones ... ¡Pero ahorra más líneas de código innecesario para envolver 1-3 líneas de lógica real en algunas clases, etc.! Algunos ingenieros piensan que si el número de líneas es más pequeño, 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 pregunta. Ignora esta sección si no estás interesado

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 ellos son capturados.

1.2. Puede usar lambda para valores de captura "por valor". En tal caso, los vars capturados se copiarán en el objeto de función (cierre).

[captureVar1,captureVar2](int arg1){}

1.3. Puedes capturar ser referencia. & - en este contexto, referencia media, no punteros.

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

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

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

Capture todos los vars no estáticos por referencia, pero mediante la captura de valores Param2

[&,Param2](int arg1){} 

2. Deducció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 devolución debe especificarse mediante el tipo de devolución final.   Además, se puede aplicar una sintaxis similar a funciones automáticas y funciones de miembros

3. Valores capturados. Lo que no puedes capturar

3.1. Puede capturar solo vars locales, no la variable miembro del objeto.

4. onversions

4.1. lambda no es un puntero de función y no es una función anónima, pero puede convertirse implícitamente en un puntero de función.

PD. 

  1. Se puede encontrar más información sobre la gramática lambda en el borrador de Trabajo para Lenguaje de Programación C ++ # 337, 2012-01-16, 5.1.2. Lambda Expressions, p.88

  2. En C ++ 14, se agregó la función extra que se ha denominado "captura de inicio". Permite realizar una 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);};
    

30
2018-06-03 16:40



Una función lambda es una función anónima que crea en línea. Puede capturar variables como algunos han explicado, (p. 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);
}

puedes escribir una función en el lugar para usarla como la que pasaste 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 las limitaciones en el estándar C ++ 11. Si desea utilizar capturas, debe confiar en la biblioteca y

#include <functional> 

(o algún otro algoritmo de biblioteca STL 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');
    });
}

12
2018-03-10 22:36



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

What is a lambda expression? 

UN expresión lambda, a veces también se conoce como lambda   función o (estrictamente hablando incorrectamente, pero coloquialmente) como    lambda, es una notación simplificada para definir y usar un objeto de función anónima. En lugar de definir una clase con nombre con un operador (), luego hacer un objeto de esa clase, y finalmente   invocándolo, 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 conocen como 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 para su código y una forma de hacerlo más atractivo. Como 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 necesitas puedes nombrar lambda expression como abajo:

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 suponga 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

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 lambdas no requieren acceso a su entorno local, podemos usarlo.

Cita del libro:

El primer carácter de una expresión lambda es siempre [. A lambda   introductor puede tomar varias formas:

[]: una lista de captura vacía Esta   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. Todos los nombres locales pueden ser utilizados. Todas las variables locales son   accedido por referencia.

[=]: implícitamente captura por valor Todo local   nombres pueden ser usados Todos los nombres se refieren a copias de las variables locales   tomado 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, almacenadas en el objeto) por referencia o por valor. Las variables con nombres precedidos por & son capturadas por   referencia. Otras variables son capturadas por valor. Una lista de captura puede   también contiene esto y nombres seguidos por ... 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 ser precedidos por &. Variables nombradas en   la lista de captura se captura por valor.

[=, lista de captura]: capture 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 estar precedidos por &. Las variables nombradas en la lista de captura se capturan por referencia.

Tenga en cuenta que un nombre local precedido por & siempre es capturado por   referencia y un nombre local no precedido por & siempre es capturado por   valor. Solo la captura por referencia permite la modificación de variables en   el entorno de llamadas.

Additional

Lambda expression formato

enter image description here

Referencias adicionales:


6
2017-11-09 11:02



Un problema que soluciona: Código más simple que lambda para una llamada en constructor que usa una función de parámetro de salida para inicializar un miembro de la clase

Puede inicializar un miembro constante de su clase, con una llamada a una función que establece su valor devolviendo su resultado como un parámetro de salida.


1
2018-06-27 00:38



Bueno, un uso práctico que he descubierto es la reducción del 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 por diferente 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 con este requisito y lo uso para ese caso.


1
2017-11-23 09:16