it-swarm-es.tech

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

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

1337
Nawaz

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 le gustaría 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 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 template en C++ 03.

La nueva solucion

C++ 11 introduce lambdas que le permiten escribir un funtor anónimo en línea para reemplazar el 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 devolución

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

"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 (el [] 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 por referencia y 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

La operator() generada es const de forma predeterminada, con la implicación de que las capturas serán const cuando acceda a ellas de forma predeterminada. Esto tiene el efecto de que cada llamada con la misma entrada produciría el mismo resultado, sin embargo, puede marcar lambda como mutable para solicitar que la operator() que se produce no sea const.

1354
Flexo

¿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é desde el 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 funcion

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

Devolución del tipo de devolució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 que han sido capturados por valor.

Casos de uso

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

C++ 14

En C++ 14 las lambdas se han ampliado con 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 plantilla de tipo en algún lugar del ámbito circundante):

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 forma return expression;. Esto también se extiende a las lambdas.

789
pmr

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 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 del &), puede mover el código a una ubicación externa y convertirlo en una función normal.

De manera similar, puede usar 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 nombradas anidadas , 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.

162
nobar

Respuestas

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

A: Debajo del capó, es el objeto de una clase generada automáticamente con sobrecarga operator () const . Dicho objeto se llama closing y es 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 custom add, subrtact operations ... 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 cubiertos por la 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 utilizar lambda para los valores de captura "por valor". En tal caso, las variables capturadas se copiarán en el 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 todas las vars no estáticas 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. Deducción por tipo de retorno

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. Conversiones

4.1 !! Lambda no es un puntero de función y no es una función anónima, pero capture-less lambdas puede convertirse implícitamente en un puntero de función.

p.s.

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

Una función lambda es una función anónima que creas en línea. Puede capturar variables como algunos 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');
    });
}

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

Una de las mejores explicaciones de lambda expression se da del autor de C++ Bjarne Stroustrupen 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 utilizando una objeto de función anónima. En lugar de definir una clase nombrada con un operador (), más tarde creando un objeto de esa clase y finalmente invocándola, 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. La expresión Lambda 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 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 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

1

1

1

1

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

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ícita 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 las variables locales que se capturarán (es decir, se almacenan en el objeto) por referencia o por valor. Las variables con nombres precedidos por & son capturado por referencia. Otras variables son capturadas por valor. Una lista de captura también puede contener esto y 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 captura Las listas son capturadas 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 el Las listas 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 format

 enter image description here

Referencias adicionales:

10
gbk

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.

2
Misgevolution

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 const

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.

1
sergiol

Los lambda en c ++ se tratan como "en función de la función disponible". Sí, está literalmente en movimiento, lo define; utilízalo 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 cualquier lugar posible. el ejemplo y lo que es lambda se puede encontrar aquí https://en.cppreference.com/w/cpp/language/lambda

describiré lo que no está ahí pero es esencial saber para cada programador de c ++

Lambda no debe utilizarse en todas partes y no se pueden reemplazar todas las funciones 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á en la reducción del número de líneas en algunos casos. Básicamente, se puede usar para la sección de código, que se llama en la misma función una o más veces y esa pieza de código no se necesita en ningún otro lugar para que pueda crear una función independiente para ella.

A continuación se muestra el ejemplo básico de lambda y lo que sucede en el fondo.

Código de usuario:

int main()
{
  // Lambda & auto
  int member=10;
  auto endGame = [=](int a, int b){ return a+b+member;};

  endGame(4,5);

  return 0;

}

Como la compilación lo expande:

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

para que pueda ver, qué tipo de sobrecarga se agrega cuando lo usa. así que no es una buena idea usarlos en todas partes. Puede ser utilizado en lugares donde sean aplicables.

0
Sachin Nale