Lambdas: de C ++ 11 a C ++ 20. Parte 2

Hola habrovsk En relación con el inicio del reclutamiento en un nuevo grupo en el curso "Desarrollador C ++" , compartimos con ustedes la traducción de la segunda parte del artículo "Lambdas: de C ++ 11 a C ++ 20". La primera parte se puede leer aquí .



En la primera parte de la serie, observamos las lambdas en términos de C ++ 03, C ++ 11 y C ++ 14. En este artículo, describí las motivaciones detrás de esta poderosa característica de C ++, uso básico, sintaxis y mejoras en cada uno de los estándares del lenguaje. También mencioné algunos casos límite.
Ahora es el momento de pasar a C ++ 17 y echar un vistazo al futuro (¡muy cerca!): C ++ 20.

Entrada

Un recordatorio rápido: la idea de esta serie surgió después de una de nuestras recientes reuniones del Grupo de usuarios de C ++ en Cracovia.

Tuvimos una sesión de programación en vivo sobre la "historia" de las expresiones lambda. La conversación fue realizada por el experto en C ++ Thomas Kaminsky ( ver el perfil de Linkedin de Thomas ). Aquí está el evento:
Lambdas: de C ++ 11 a C ++ 20 - Grupo de usuarios de C ++ Cracovia .

Decidí tomar el código de Thomas (¡con su permiso!) Y escribir artículos basados ​​en él. En la primera parte de la serie, hablé sobre las expresiones lambda de la siguiente manera:

  • Sintaxis básica
  • Tipo lambda
  • Operador de llamadas
  • Capturar variables (variables mutables, globales, estáticas, miembros de clase y este puntero, objetos que solo se pueden mover, almacenar constantes):

    • Tipo de retorno
    • IIFE - Expresión de función invocada inmediatamente
    • Conversión a un puntero de función
    • Tipo de retorno
    • IIFE - Expresiones invocadas inmediatamente
    • Convertir a un puntero de función
  • Mejoras en C ++ 14

    • Salida de tipo de retorno
    • Captura con inicializador
    • Capturar una variable miembro
    • Expresiones genéricas lambda

¡La lista anterior es solo una parte de la historia de las expresiones lambda!

¡Ahora veamos qué ha cambiado en C ++ 17 y qué obtenemos en C ++ 20!

Mejoras en C ++ 17

Estándar (borrador antes de la publicación) Sección N659 sobre lambdas: [expr.prim.lambda] . C ++ 17 trajo dos mejoras significativas a las expresiones lambda:

  • constexpr lambda
  • Capture * esto

¿Qué significan estas innovaciones para nosotros? Vamos a resolverlo.

constexpr expresiones lambda

Comenzando con C ++ 17, el estándar define implícitamente operator() para un tipo lambda como constexpr , si es posible:
De expr.prim.lambda # 4 :
El operador de llamada de función es una función constexpr si la declaración del parámetro de condición de la expresión lambda correspondiente es seguida por constexpr, o si cumple los requisitos para la función constexpr.

Por ejemplo:

 constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4); 

Recuerde que en C ++ 17 constexpr función debe seguir estas reglas:

  • no debe ser virtual;

    • su tipo de retorno debe ser un tipo literal;
    • cada uno de los tipos de sus parámetros debe ser un tipo literal;
    • su cuerpo debe ser = eliminar, = predeterminado o una declaración compuesta que no contenga
      • definiciones asm
      • ir a expresiones,
      • etiquetas
      • intente bloquear o
      • La definición de una variable no literal, una variable estática o una variable de memoria de transmisión para la cual no se realiza la inicialización.

¿Qué tal un ejemplo más práctico?

 template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); } 

Puedes jugar con el código aquí: @Wandbox

El código usa constexpr lambda, y luego se pasa al algoritmo simple SimpleAccumulate . El algoritmo usa varios elementos de C ++ 17: las adiciones constexpr a std::array , std::begin y std::end (usadas en un bucle for con un rango) ahora también son constexpr , por lo que esto significa que todo el código puede ejecutarse en tiempo de compilación.

Por supuesto, esto no es todo.

Puede capturar variables (siempre que también sean constexpr ):

 constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); } 

Pero hay un caso interesante en el que no pasa más la variable capturada, por ejemplo:

 constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x }; 

En este caso, en Clang podemos obtener la siguiente advertencia:

warning: lambda capture 'x' is not required to be captured for this use

Esto probablemente se deba al hecho de que x se puede cambiar en su lugar con cada uso (a menos que lo transfiera más o tome la dirección de este nombre).

Pero por favor dígame si conoce las reglas oficiales para este comportamiento. Solo encontré (de cppreference ) (pero no puedo encontrarlo en el borrador ...)

(Nota del traductor: mientras escriben nuestros lectores, probablemente me refiero a sustituir el valor de 'x' en cada lugar donde se usa. Definitivamente es imposible cambiarlo).

Una expresión lambda puede leer el valor de una variable sin capturarla si la variable
* tiene un entero constante non-volatile o un tipo enumerado y se ha inicializado con constexpr o
* es constexpr y no tiene miembros mutables.

Prepárate para el futuro:

En C ++ 20, tendremos algoritmos estándar constexpr y posiblemente incluso algunos contenedores, por lo que constexpr lambdas será muy útil en este contexto. ¡Su código se verá igual para la versión en tiempo de ejecución, así como para la versión constexpr (versión en tiempo de compilación)!

En pocas palabras:

constexpr lambda le permite ser coherente con la programación repetitiva y posiblemente tener un código más corto.

Ahora pasemos a la segunda característica importante disponible en C ++ 17:

Captura de * esto
Capture * esto

¿Recuerdas nuestro problema cuando queríamos capturar a un miembro de la clase? De forma predeterminada, capturamos esto (como un puntero), y por lo tanto, podemos tener problemas cuando los objetos temporales quedan fuera de alcance ... Esto se puede solucionar utilizando el método de captura con un inicializador (consulte la primera parte de la serie). Pero ahora, en C ++ 17, tenemos una forma diferente. Podemos envolver una copia de * esto:

 #include <iostream> struct Baz { auto foo() { return [*this] { 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

La captura de la variable miembro deseada mediante la captura con el inicializador lo protege de posibles errores con valores temporales, pero no podemos hacer lo mismo cuando queremos llamar a un método como:

Por ejemplo:

 struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; }; 

En C ++ 14, la única forma de hacer que el código sea más seguro es capturar this con un inicializador:

 auto foo() { return [self=*this] { self.print(); }; }   C ++ 17    : auto foo() { return [*this] { print(); }; } 

Una cosa mas:

Tenga en cuenta que si escribe [=] en una función miembro, ¡ this captura implícitamente! Esto puede conducir a errores en el futuro ... y quedará obsoleto en C ++ 20.

Entonces llegamos a la siguiente sección: el futuro.

El futuro con C ++ 20

En C ++ 20, obtenemos las siguientes funciones:

  • Permita [=, this] como captura lambda - P0409R2 y cancele la captura implícita de esto a través de [=] - P0806
  • Extensión del paquete en lambda init-capture: ... args = std::move (args)] () {} - P0780
  • thread_local estática, thread_local y lambda para enlaces estructurados - P1091
  • patrón lambda (también con conceptos) - P0428R2
  • Simplificando la captura implícita de Lambda - P0588R1
  • Lambda constructivo y asignable sin guardar el estado predeterminado - P0624R2
  • Lambdas en un contexto no calculado - P0315R4

En la mayoría de los casos, las funciones recién introducidas "borran" el uso de lambda y permiten algunos casos de uso avanzados.

Por ejemplo, con P1091 puede capturar un enlace estructurado.

También tenemos aclaraciones relacionadas con la captura de esto. En C ++ 20, recibirá una advertencia si captura [=] en un método:

 struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20 

Si realmente necesita capturar esto, debe escribir [=, this] .

También hay cambios relacionados con casos de uso avanzados, como contextos sin estado y lambdas sin estado que se pueden construir de forma predeterminada.

Con ambos cambios puedes escribir:

 std::map<int, int, decltype([](int x, int y) { return x > y; })> map; 

Lea los motivos de estas características en la primera versión de las oraciones: P0315R0 y P0624R0 .

Pero veamos una característica interesante: las plantillas lambda.

Patrón Lambd

En C ++ 14, obtuvimos lambdas generalizadas, lo que significa que los parámetros declarados como automáticos son parámetros de plantilla.

Para lambda:

 [](auto x) { x; } 

El compilador genera una declaración de llamada que coincide con el siguiente método repetitivo:

 template<typename T> void operator(T x) { x; } 

Pero no había forma de cambiar este parámetro de plantilla y usar los argumentos de plantilla reales. En C ++ 20, esto será posible.

Por ejemplo, ¿cómo podemos limitar nuestra lambda para que funcione solo con vectores de algún tipo?

Podemos escribir una lambda general:

 auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

Pero si lo llama con un parámetro int (por ejemplo, foo(10); ), puede obtener un error difícil de leer:

 prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n'; 

En C ++ 20 podemos escribir:

 auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

El lambda anterior permite la declaración de llamada de plantilla:

 <typename T> void operator(std::vector<T> const& s) { ... } 

El parámetro de plantilla sigue la cláusula de captura [] .

Si lo llama con int (foo(10);) , recibirá un mensaje más agradable:

 note: mismatched types 'const std::vector<T>' and 'int' 


Puedes jugar con el código aquí: @Wandbox

En el ejemplo anterior, el compilador puede advertirnos sobre inconsistencias en la interfaz lambda que en el código dentro del cuerpo.

Otro aspecto importante es que en una lambda universal solo tiene una variable, no su tipo de plantilla. Por lo tanto, si desea acceder a él, debe usar decltype (x) (para una expresión lambda con el argumento (auto x)). Esto hace que algunos códigos sean más detallados y complicados.

Por ejemplo (usando el código de P0428):

 auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; } 

Ahora puedes escribir como:

 auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; } 

En la sección anterior, tuvimos una breve descripción de C ++ 20, pero tengo otro caso de uso adicional para usted. Esta técnica es incluso posible en C ++ 14. Así que sigue leyendo.

Bono - LEVANTAMIENTO con lambdas

Actualmente tenemos un problema cuando tiene sobrecargas de funciones y desea pasarlas a algoritmos estándar (o cualquier cosa que requiera algún objeto llamado):

 // two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); } 

Recibimos el siguiente error de GCC 9 (troncal):

 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^ 

Sin embargo, hay un truco en el que podemos usar un lambda y luego llamar a la función de sobrecarga deseada.

En forma básica, para tipos de valores simples, para nuestras dos funciones, podemos escribir el siguiente código:

 std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 

Y en la forma más general, necesitamos escribir un poco más:

 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \ -> decltype(foo(std::forward<decltype(x)>(x)...)) \ { return foo(std::forward<decltype(x)>(x)...); } 

Código bastante complicado ... ¿verdad? :)

Intentemos descifrarlo:

Creamos una lambda genérica y luego pasamos todos los argumentos que obtenemos. Para determinarlo correctamente, necesitamos especificar noexcept y el tipo del valor de retorno. Es por eso que tenemos que duplicar el código de llamada, para obtener los tipos correctos.
Tal macro LIFT funciona en cualquier compilador que soporte C ++ 14.

Puedes jugar con el código aquí: @Wandbox

Conclusión

En esta publicación, observamos cambios significativos en C ++ 17 y ofrecimos una descripción general de las nuevas características en C ++ 20.

Puede notar que con cada iteración del lenguaje, las expresiones lambda se mezclan con otros elementos de C ++. Por ejemplo, antes de C ++ 17, no podíamos usarlos en el contexto de constexpr, pero ahora es posible. Del mismo modo, con lambdas genéricas que comienzan con C ++ 14 y su evolución hacia C ++ 20 en forma de lambdas plantilla. ¿Me estoy perdiendo algo? Tal vez tienes algún ejemplo emocionante? Por favor, hágamelo saber en los comentarios!

Referencias

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
C ++ 17 - [expr.prim.lambda]
Expresiones Lambda en C ++ | Documentos de Microsoft
Simon Brand: pasar conjuntos de sobrecarga a funciones
Jason Turner - C ++ Weekly - Ep 128 - Sintaxis de plantilla de C ++ 20 para Lambdas
Jason Turner - C ++ Weekly - Ep 41 - Ctex 17's constexpr Lambda Support

Invitamos a todos al tradicional seminario web gratuito sobre el curso, que tendrá lugar mañana 14 de junio.

Source: https://habr.com/ru/post/455978/


All Articles