Buenas tardes amigos. Hoy hemos preparado para usted una traducción de la primera parte del artículo
"Lambdas: de C ++ 11 a C ++ 20" . La publicación de este material está programada para coincidir con el lanzamiento del curso
"C ++ Developer" , que comienza mañana.
Las expresiones Lambda son una de las adiciones más poderosas en C ++ 11 y continúan evolucionando con cada nuevo estándar de lenguaje. En este artículo, repasaremos su historia y veremos la evolución de esta parte importante del C ++ moderno.

La segunda parte está disponible aquí:
Lambdas: de C ++ 11 a C ++ 20, parte 2EntradaEn una reunión local del grupo de usuarios de C ++, tuvimos una sesión de programación en vivo sobre el "historial" de las expresiones lambda. La conversación fue dirigida por el experto en C ++ Tomasz Kamiński (
ver el perfil de Thomas Linkedin ). Aquí está el evento:
Lambdas: de C ++ 11 a C ++ 20 - Grupo de usuarios de C ++ CracoviaDecidí tomar el código de Thomas (¡con su permiso!), Describirlo y crear un artículo separado.
Comenzaremos explorando C ++ 03 y la necesidad de expresiones funcionales locales compactas. Luego pasamos a C ++ 11 y C ++ 14. En la segunda parte de la serie, veremos cambios en C ++ 17 e incluso echaremos un vistazo a lo que sucederá en C ++ 20.
Lambdas en C ++ 03Desde el principio, los
std::algorithms
STL, como
std::sort
, podrían tomar cualquier objeto llamado y llamarlo en elementos contenedores. Sin embargo, en C ++ 03, esto solo involucraba punteros a funciones y functores.
Por ejemplo:
#include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Código de ejecución:
@WandboxPero el problema era que tenía que escribir una función o functor por separado en un alcance diferente, y no en el alcance de la llamada al algoritmo.
Como una posible solución, puede considerar escribir una clase de functor local, ya que C ++ siempre admite esta sintaxis. Pero no funciona ...
Echa un vistazo a este código:
int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Intente compilarlo con
-std=c++98
y verá el siguiente error en GCC:
error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor'
Esencialmente, en C ++ 98/03, no puede crear una instancia de una plantilla con un tipo local.
Debido a todas estas limitaciones, el Comité comenzó a desarrollar una nueva característica que podemos crear y llamar "en su lugar" ... "expresiones lambda".
Si miramos
N3337 , la versión final de C ++ 11, veremos una sección separada para lambdas:
[expr.prim.lambda] .
Junto a C ++ 11Creo que las lambdas se han agregado al lenguaje sabiamente. Usan la nueva sintaxis, pero luego el compilador la "extiende" a una clase real. Por lo tanto, tenemos todas las ventajas (y a veces desventajas) de un lenguaje estrictamente mecanografiado.
Aquí hay un ejemplo de código básico que también muestra el objeto functor local correspondiente:
#include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); }
Ejemplo:
@WandBoxTambién puede consultar CppInsights, que muestra cómo el compilador extiende el código:
Echa un vistazo a este ejemplo:
CppInsighs: prueba lambdaEn este ejemplo, el compilador convierte:
[] (int x) { std::cout << x << '\n'; }
En algo similar a esto (forma simplificada):
struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance;
Sintaxis de la expresión lambda:
[] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | | |
Algunas definiciones antes de comenzar:
De
[expr.prim.lambda # 2] :
La evaluación de una expresión lambda da como resultado un valor temporal. Este objeto temporal se llama
objeto de cierre .
Y de
[expr.prim.lambda # 3] :
El tipo de expresión lambda (que también es el tipo de un objeto de cierre) es un tipo único sin unión sin nombre de la clase llamada
tipo de cierre .
Algunos ejemplos de expresiones lambda:
Por ejemplo:
[](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; }
Tipo lambdaComo el compilador genera un nombre único para cada lambda, no es posible conocerlo de antemano.
auto myLambda = [](int a) -> double { return 2.0 * a; }
Además
[expr.prim.lambda] :
El tipo de cierre asociado con la expresión lambda tiene un constructor predeterminado remoto ([dcl.fct.def.delete]) y un operador de asignación remota.
Por lo tanto, no puedes escribir:
auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy;
Esto provocará el siguiente error en GCC:
error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor
Operador de llamadasEl código que coloca en el cuerpo lambda se "traduce" al código de operador () del tipo de cierre correspondiente.
Por defecto, este es un método constante incorporado. Puede cambiarlo especificando mutable después de declarar los parámetros:
auto myLambda = [](int a) mutable { std::cout << a; }
Aunque el método constante no es un "problema" para una lambda sin una lista de captura vacía ... es importante cuando desea capturar algo.
Capturar[] no solo introduce una lambda, sino que también contiene una lista de variables capturadas. Esto se llama una lista de captura.
Al capturar una variable, crea un miembro de copia de esta variable en el tipo de cierre. Luego, dentro del cuerpo lambda, puede acceder a él.
La sintaxis básica es:
- [&] - captura por referencia, todas las variables en almacenamiento automático se declaran en alcance
- [=] - captura por valor, el valor se copia
- [x, & y]: captura explícitamente x por valor e y por referencia
Por ejemplo:
int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; }
Puedes jugar con el ejemplo completo aquí:
@WandboxAunque especificar
[=]
o
[&]
puede ser conveniente, ya que captura todas las variables en el almacenamiento automático, es más obvio capturar variables explícitamente. Por lo tanto, el compilador puede advertirle sobre efectos no deseados (ver, por ejemplo, notas sobre variables globales y estáticas)
También puede leer más en el párrafo 31 de Effective Modern C ++ de Scott Meyers: "Evite los modos de captura predeterminados".
Y una cita importante:
Los cierres de C ++ no aumentan la vida útil de los enlaces capturados.
MutableDe forma predeterminada, el operador de tipo de cierre () es constante y no puede modificar las variables capturadas dentro del cuerpo de una expresión lambda.
Si desea cambiar este comportamiento, debe agregar la palabra clave mutable después de la lista de parámetros:
int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl;
En el ejemplo anterior, podemos cambiar los valores de x e y ... pero estas son solo copias de x e y del alcance adjunto.
Captura de variable globalSi tiene un valor global y luego usa [=] en una lambda, podría pensar que el valor global también es capturado por el valor ... pero no lo es.
int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); }
Puedes jugar con el código aquí:
@Wandbox
Solo se capturan las variables en el almacenamiento automático. GCC puede incluso emitir la siguiente advertencia:
warning: capture of variable 'global' with non-automatic storage duration
Esta advertencia solo aparecerá si captura explícitamente la variable global, por lo que si usa
[=]
, el compilador no lo ayudará.
El compilador de Clang es más útil ya que genera un error:
error: 'global' cannot be captured because it does not have automatic storage duration
Ver
@WandboxCapturando variables estáticasLa captura de variables estáticas es similar a la captura global:
#include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); }
Puedes jugar con el código aquí:
@WandboxConclusión
10 11 12
Y de nuevo, solo aparecerá una advertencia si captura explícitamente una variable estática, por lo que si usa
[=]
, el compilador no lo ayudará.
Captura de miembros de la clase¿Sabes qué sucede después de ejecutar el siguiente código:
#include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
El código declara un objeto Baz y luego llama a
foo()
. Tenga en cuenta que
foo()
devuelve un lambda (almacenado en
std::function
) que captura a un miembro de la clase.
Como usamos objetos temporales, no podemos estar seguros de lo que sucederá cuando se llame a f1 y f2. Este es un problema de enlace colgante que causa un comportamiento indefinido.
Del mismo modo:
struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo();
Juega con el código
@WandboxNuevamente, si especifica captura explícitamente ([s]):
std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; }
El compilador evitará su error:
In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ...
Vea un ejemplo:
@WandboxObjetos que solo se pueden moverSi tiene un objeto que solo se puede mover (por ejemplo, unique_ptr), no puede colocarlo en una lambda como una variable capturada. La captura por valor no funciona, por lo que solo puede capturar por referencia ... sin embargo, esto no se lo transferirá a usted, y probablemente esto no sea lo que deseaba.
std::unique_ptr<int> p(new int[10]); auto foo = [p] () {};
Guardar constantesSi captura una variable constante, se conserva la constancia:
int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo();
Ver código:
@WandboxTipo de retornoEn C ++ 11, puede omitir el
trailing
tipo lambda de retorno, y luego el compilador lo generará por usted.
Inicialmente, la salida del tipo de valor de retorno se limitaba a lambdas que contenían una declaración de retorno, pero esta restricción se eliminó rápidamente, ya que no hubo problemas con la implementación de una versión más conveniente.
Consulte los
informes de defectos del lenguaje central estándar de C ++ y los problemas aceptados (¡gracias a Thomas por encontrar el enlace correcto!)
Por lo tanto, comenzando con C ++ 11, el compilador puede inferir el tipo del valor de retorno si todas las declaraciones de retorno se pueden convertir al mismo tipo.
Si todas las declaraciones de retorno devuelven la expresión y los tipos de retorno después de la conversión lvalue-to-rvalue (7.1 [conv.lval]), array-to-pointer (7.2 [conv.array]) y function-to-pointer (7.3 [conv. func]) es lo mismo que el tipo genérico;
auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; };
Puedes jugar con el código aquí:
@WandboxHay dos
return
en el lambda anterior, pero todas apuntan al
double
, por lo que el compilador puede inferir el tipo.
IIFE - Expresión de función invocada inmediatamenteEn nuestros ejemplos, definí un lambda y luego lo llamé usando el objeto de cierre ... pero también se puede llamar de inmediato:
int x = 1, y = 1; [&]() { ++x; ++y; }();
Tal expresión puede ser útil en la inicialización compleja de objetos constantes.
const auto val = []() { }();
Escribí más sobre esto en la
publicación IIFE para Inicialización compleja .
Convertir a un puntero de funciónEl tipo de cierre para una expresión lambda sin captura tiene una función implícita abierta no virtual de convertir una constante en un puntero a una función que tiene el mismo parámetro y tipos de retorno que el operador de llamar a una función del tipo de cierre. El valor devuelto por esta función de conversión debe ser la dirección de la función, que cuando se llama tiene el mismo efecto que llamar al operador de una función de un tipo similar a un tipo de cierre.
En otras palabras, puede convertir lambdas sin capturas a un puntero de función.
Por ejemplo:
#include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); }
Puedes jugar con el código aquí:
@WandboxMejoras en C ++ 14N4140 estándar y lambda:
[expr.prim.lambda] .
C ++ 14 agregó dos mejoras significativas a las expresiones lambda:
- Capturas con Initializer
- Lambdas comunes
Estas características resuelven varios problemas que eran visibles en C ++ 11.
Tipo de retornoLa salida del tipo de valor de retorno de la expresión lambda se ha actualizado para cumplir con las reglas de salida automática para funciones.
[expr.prim.lambda # 4]El tipo de retorno del lambda es auto, que se reemplaza por el tipo de retorno final, si se proporciona y / o se deduce de las declaraciones de retorno, como se describe en [dcl.spec.auto].
Capturas con InitializerEn resumen, podemos crear una nueva variable miembro del tipo de cierre y luego usarla dentro de la expresión lambda.
Por ejemplo:
int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); }
Esto puede resolver varios problemas, por ejemplo, con tipos que solo están disponibles para mover.
En movimientoAhora podemos mover el objeto a un miembro del tipo de cierre:
#include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; }
OptimizaciónOtra idea es usarlo como una técnica de optimización potencial. En lugar de calcular algún valor cada vez que llamamos al lambda, podemos calcularlo una vez en el inicializador:
#include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); }
Capturar una variable miembroUn inicializador también se puede utilizar para capturar una variable miembro. Luego podemos obtener una copia de la variable miembro y no preocuparnos por los enlaces colgantes.
Por ejemplo:
struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
Puedes jugar con el código aquí:
@Wandbox
En
foo()
capturamos una variable miembro copiándola al tipo de cierre. Además, usamos auto para generar el método completo (anteriormente, en C ++ 11 podíamos usar
std::function
).
Expresiones genéricas lambdaOtra mejora significativa es la lambda generalizada.
Comenzando con C ++ 14 puedes escribir:
auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world");
Esto es equivalente a usar una declaración de plantilla en una declaración de llamada del tipo de cierre:
struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance;
Tal lambda generalizada puede ser muy útil cuando es difícil inferir un tipo.
Por ejemplo:
std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } };
¿Me equivoco aquí? ¿La entrada tiene el tipo correcto?
.
.
.
Probablemente no, ya que el tipo de valor para std :: map es
std::pair<const Key, T>
. Entonces mi código hará copias adicionales de las líneas ...
Esto se puede solucionar con
auto
:
std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } );
Puedes jugar con el código aquí:
@WandboxConclusiónQue historia!
En este artículo, comenzamos desde los primeros días de expresiones lambda en C ++ 03 y C ++ 11 y pasamos a una versión mejorada en C ++ 14.
Viste cómo crear una lambda, cuál es la estructura básica de esta expresión, qué es una lista de captura y mucho más.
En la siguiente parte del artículo, pasaremos a C ++ 17 y conoceremos las características futuras de C ++ 20.
La segunda parte está disponible aquí:
Lambdas: de C ++ 11 a C ++ 20, parte 2
Referencias
C ++ 11 -
[expr.prim.lambda]C ++ 14 -
[expr.prim.lambda]Expresiones Lambda en C ++ | Documentos de MicrosoftDesmitificación de lambdas C ++ - Sticky Bits - Desarrollado por Feabhas; Sticky Bits - Desarrollado por Feabhas
Esperamos sus comentarios e invitamos a todos los interesados en el curso
"Desarrollador C ++" .