¿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 era posible antes de su introducción?

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

Author: hugomg, 2011-10-02

8 answers

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 engorrosos de usar, particularmente si el funtor que desea 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 exagerado escribir toda una clase solo para hacer algo trivial y único.

En C++03 podría estar tentado a 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 template en C++03.

La nueva solución

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 limpio de leer (mantiene todo en un solo lugar) y potencialmente más simple de mantener, por ejemplo, en el más simple formulario:

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 se deduce el tipo de retorno de la lambda para 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 tipo devuelto no puede ser deducido por el compilador, 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;
            }
        });
}

" Capturar " variables

Hasta ahora no hemos usado nada más que lo que se le 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 utilizar la cláusula capture (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 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 epsilon por referencia

El operator() generado es const por defecto, con la implicación de que las capturas serán const cuando accedas a ellas por predeterminado. 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 solicitar que el operator() que se produce no es const.

 1239
Author: Flexo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-14 09:50:26

¿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 programación real, no en 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 toda su gloria

[]() 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 lo que 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 el ámbito de aplicación 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 lo mismo que en cualquier otra función de C++.

El cuerpo de la función

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

Deducción de tipo de devolución

Si una lambda solo tiene una sentencia return, el tipo return puede ser omitido y tiene el tipo implícito de decltype(return_statement).

Mutable

Si una lambda está marcada como mutable (por ejemplo, []() mutable { }), se permite mutar los valores que han sido capturados por value.

Casos de uso

El la biblioteca definida por el estándar ISO se beneficia en gran medida de lambdas y aumenta la usabilidad 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 lambdas han sido extendidas por varias propuestas.

Capturas Lambda inicializadas

Un elemento de la lista de captura ahora se puede inicializar con =. Esto permite renombrar las variables y capturarlas moviéndose. Un ejemplo tomado de la 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 eran un argumento de plantilla de tipo en algún lugar del ámbito circundante):

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

Deducción Mejorada del Tipo de Retorno

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.

 731
Author: pmr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-03 09:26:10

Las expresiones Lambda se usan típicamente 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 que las expresiones lambda sean 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 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 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 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 le permiten crear con nombre funciones 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 a la vista (en comparación con lambdas en línea anónimas) cuando se pasa una función no trivial como un parámetro a otra función. Nota: no olvide el punto y coma después de la llave rizada 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 sobrecarga de inicialización significativa para el objeto de función, puede optar por reescribir esto como una función normal.

 153
Author: nobar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-24 09:22:53

Respuestas

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

A: Bajo el capó, es el objeto de una clase autogenerada con sobrecarga operator() const. Dicho objeto se llama closure y es creado por el compilador. Este concepto de 'cierre' está cerca del concepto bind de C++11. Pero las lambdas normalmente generan mejor código. Y las llamadas a través de cierres permiten la entrada completa.

P: ¿Cuándo usaría uno?

A: Definir " simple y small logic " y ask compiler realizan la generación a partir de la pregunta anterior. Le das a un compilador algunas expresiones que quieres que estén dentro de operator (). Todas las demás cosas compilador generará a usted.

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 operadores sobrecargando en lugar de funciones para operaciones personalizadas add, subrtact...Pero guarda más líneas de código innecesario para envolver 1-3 líneas de lógica real para algunas clases, etc.! Algunos ingenieros piensan que si el número de líneas es menor, entonces hay menos posibilidades de cometer errores en él (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 var capturados se copiarán al objeto de función (closure).

[captureVar1,captureVar2](int arg1){}

1.3. Puede capturar ser referencia. & context 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 notación para capturar todos los var no estáticos por valor, o por referencia y especificar smth. mas. Ejemplos: Capture todos los vars no estáticos por valor, pero por referencia capture Param2

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

Capture todos los var no estáticos por referencia, sino por valor capture Param2

[&,Param2](int arg1){} 

2. Deducción de 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, la sintaxis similar puede ser aplicado a funciones automáticas y funciones miembro

3. Valores capturados. Lo que no puedes capturar

3.1. Solo puede capturar VAR locales, no variables miembro del objeto.

4. Conversiones

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

P. s.

  1. Se puede encontrar más información sobre la gramática lambda in Working draft for Programming Language C++ #337, 2012-01-16, 5.1.2. Expresiones Lambda, p.88

  2. En C++14 se ha agregado la característica adicional que se ha nombrado como "init capture". Permite realizar la declaración arbitral de los 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
Author: bruziuz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-22 21:39:58

Una función lambda es una función anónima que se crea 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 pasa a 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 quieres utilice capturas, usted tiene que 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');
    });
}
 12
Author: Ted,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-11 00:16:13

Una de las mejores explicaciones de lambda expression se da 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?

A expresión lambda , a veces también referida como a lambda función o (estrictamente hablando incorrectamente, pero coloquialmente) como un lambda , es una notación simplificada para definir y usar un objeto de función anónimo . En su lugar de definir una clase con nombre con un operador (), más tarde 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 un 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 callbacks.

What class of problem do they solve that wasn't possible prior to their introduction?

Aquí supongo que cada acción hecho 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 optimizar tu 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 u necesita u puede 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 asumir 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;

[] - esto es capture list o lambda introducer: si lambdas no requieren acceso a su entorno local, podemos usar se.

Cita del libro:

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

[]: una lista de captura vacía. Este 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.

[&]: implícitamente capturada por referencia. Todos los locales se pueden usar nombres. Todas las variables locales son acceso por referencia.

[=]: captura implícita por valor. Todos los locales se pueden usar nombres. Todos los nombres se refieren a copias de las variables locales tomado en el punto de llamada de la expresión lambda.

[capture-list]: captura explícita; la capture-list 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. Variables con nombres precedidos por & son capturados por referencia. Otras variables se capturan por valor. Una lista de captura puede también contienen esto y nombres seguidos por ... 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 ir precedidos por &. Variables nombradas en el las listas de captura se capturan por valor.

[=, capture-list] : captura implícita por valore 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 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 pre-cedido por & siempre es capturado por valor. Solo captura por referencia permite la modificación de variables en vocación ambiente.

Additional

Lambda expression formato

introduzca la descripción de la imagen aquí

Referencias adicionales:

 6
Author: gbk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-09 11:11:21

Un problema que resuelve: 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 const

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

 1
Author: sergiol,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:29

Bueno, un uso práctico que he descubierto es reducir el código de la placa de 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 tenga que hacer algo para diferentes bsize casos. Por supuesto, podría crear una función, pero ¿qué pasa si desea limitar el uso dentro del alcance de la función soul user? la naturaleza de lambda cumple con este requisito y lo uso para ese caso.

 1
Author: Misgevolution,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-30 04:24:52