c++-faq funciones - ¿Qué es una expresión lambda en C++11?




expression closure (8)

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.

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



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

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


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.


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 .


En un libro que leí (no recuerdo correctamente qué libro) decía: Los compiladores intentan analizar las expresiones con el token más grande usando la regla izquierda-derecha.

En este caso, la expresión:

x-->0

Analiza los tokens más grandes:

token 1: x
token 2: --
token 3: >
token 4: 0
conclude: x-- > 0

La misma regla se aplica a esta expresión:

a-----b

Después de analizar

token 1: a
token 2: --
token 3: --
token 4: -
token 5: b
conclude: (a--)-- - b

Espero que esto ayude a entender la expresión complicada ^^







c++ lambda c++11 c++-faq