Lambda expressions en C++20 1/2

Lambda expressions en C++20 1/2

Funciones anónimas 

Las expresiones lambda construyen una closure en español "cierre". Se trata de un objecto, el cual actua como una función sin nombre (anónima) y es capaz de capturar variables en el scope donde es definida.


Se les nombra expresiones lambda o closure. Algunos les llaman funciones anónimas, aunque para el compiler, las expresiones lambda no se procesan como funciones normales. Su principal característica es la posibilidad de ser definidas como funciones "anónimas" dentro de una función, permitiendo así un "encapsulamiento" y de esta manera evitar confusiones con namespaces. El uso de las closures ayuda a proporcionar contexto adicional al definir la función tan cerca posible de donde esta será usada.


Estructura de una expresión lambda.

Su sintaxis general es moderna, como lo es C++ a partir de C++11 y se ve de la siguiente forma:


[capture-clause] ( parameters ) ->  returnType { statements; }


Otros formatos que tambien se pueden aplicar son los siguientes:


  • [ captures ] <tparams> ( params ) specifiers exception attr -> ret requires {body} 

Sintaxis en C++20. Los parámetros template <tparams>  y, specifiers exception attr -> ret requires son opcionales (solo en C++20)


  • [ captures ] ( params ) -> ret { body }

Declaración de una expresión lamda constante, los objetos capturados por value, son constantes en el body


  • [ captures ] ( params ) { body }

Omitiendo el tipo, el cual la función debe regresar, pues este puede ser deducido a partir de la declaración de un return, al igual que una función usando la palabra clave auto


  • [ captures ] { body }

Omitiendo la lista de parámetros, en este caso la función no toma argumento alguno, lo cual también se podría emular usando [ captures ] () { body }. Este formato solamente puede usarse si no se utiliza constexpr, mutable, una especificación de excepción o un tipo de retorno al final.


Su estructura

Las partes de una expresión lambda se pueden explicar según cppreference [1] de la siguiente forma


Capture

Captures es una lista de capturas que puede estar vacía y opcionalmente comienza con la captura por predeterminada. La lista de capturas define las variables externas a las se ingresa desde el interior del cuerpo de la expresion lamda. Las únicas capturas por defecto son:


  • [&] : captura implícitamente las variables usadas por referencia
  • [=] : captura automáticamente las variables usadas por valor (copia)

El puntero this es también implícitamente capturado como referencia sin importar la captura presente. En C++20, el puntero this no es capturado implícitamente, si se usa la captura por copia [=]


La sintaxis de cada captura en la lista captures puede ser de las siguientes formas:


  • [&]
    En este caso, se toman todas las capturas por referencia.
  • [&, i]
    Aquí tomamos todas las variables por referencia, a excepción de i, de la cual se tomará una copia (call by value)
  • [&, i, j]
    { al igual que en el caso anterior, aquí, la expresion lambda tomara una copia de las variables i y j};
  • [=]
    Aquí como dicho antes, tomamos una copia de las variables que deseamos usar en la expresion lambda.
  • [ =, &i ]
    Se tomarán copias de las variables a utilizar con excepción de la variable i, la cual será usada como referencia.
  • [ =, *this ]
    Esto es válido desde C++ 17, pero no en C++14 y menores.
  • [ =, this]
    Esto es válido a partir de C++20 (y será lo mismo que [=] ), pero no en versiones anteriores

Una expresión lambda puede usar una variable sin capturarla si la variable:


  1. No es una variable local o la variable tiene una duración de almacenamiento local estática o vive en un thread aparte (en el caso de que viva en un thread, la variable no puede ser capturada) 
  2. La variable es una referencia, la cual ha sido inicializada con una constant expresión

Una expresión lambda puede leer el valor de una variable sin capturarla cuando:


  1.  La variable es const pero no es de tipo volatile integral o enum y ha sido inicializada con una expresión constante (constant expresión)
  2. La variable es constexpr y no tiene miembros mutables

<tparameters>

<tparameters>: Específicamente para C++20. Es la lista de parámetros de las plantillas (templates) y es usada para proveer nombres a los parámetros template de una lamda genérica. Al igual que en una declaración de template, la lista de parámetros del template puede ser precedida por una clausula opcional, la cual especifica las restricciones en los argumentos del template. Si la lista es puesta, no puede estar vacia.


Params

Params: La lista de parámetros, de igual manera que en una funcion normal. Si la palabra auto es usada como tipo de algún parámetro, la lambda es una lambda “genérica” (esto a partir de C++14)


Specifiers

Specifiers: Es una secuencia opcional de especificadores. Los siguientes especificadores son permitidos:


  • Mutable: Permite al body modificar los objetos capturados por copia.
  • Constexpr: Especifica explícitamente que el operador para llamar la funcion es una funcion constexpr (lo cual sucede igualmente, si el especificador consexpr no está presente si es necesario para satisfacer todos los requerimientos de la constexpr funcion )
  • Consteval: Especifica que el operador para llamar la funcion es una “immediate function.”  Consteval y constexpr no pueden ser usadas al tiempo.

Exception

Exception: proporciona la especificación de excepción dinámica o el especificador noexcept para el operador () del tipo de closure


attr

attr: proporciona la especificación de atributo para el tipo de operador de llamado de la función del tipo de closure. Cualquier atributo así especificado pertenece al tipo de operador de llamada, no al operador de llamada en sí. (Por ejemplo, el atributo [[noreturn]] no se puede utilizar).


ret

ret: tipo de retorno. Si no está presente, está implícito en las declaraciones de retorno de la función (o se anula si no devuelve ningún valor)


requires

requires (C ++ 20): agrega una restricción al operador () del tipo de closure


body

body - Cuerpo funcional


Capturas predeterminadas y el identificador

Solo las expresiones lambda que han sido definidas en el bloque del scope o en algún inicializador de miembro predeterminado, pueden tener una captura predeterminada o capturas sin inicializadores.


Para dicha expresión lambda, el largo del scope es definido como el conjunto de scopes encadenados hasta la función circundante más interna, incluyendo a esta y a sus parámetros.


Esto incluye bloques de scopes encadenados (o anidados) y los scopes de otras expresiones lambdas encadenadas.


El identificador en cualquier captura sin inicializador se busca por su nombre hacia arriba en el scope de la expresion lambda y debe coincidir con una variable del mismo nombre, con duración de almacenamiento automático declarada en el scope. Solo asi es capturada una variable.


Es posible inicializar capturas en el bloque captures, en ese caso la captura se trata como una variable nueva de tipo auto, en el scope de la expresion lambda.


Como ejemplo revisaremos la siguiente expresion lambda:


1
2
3
4
5
6
int x = 4;
auto y = [&r = x, x = x + 1]()->int
    {
        r += 2;
        return x * x;
    }(); 

     El valor de x fuera del scope de la expresion lambda permanece intacto, a pesar de haber asignado otro valor al hacer x = x + 1 en la lista de capturas. En realidad, no se ha hecho ninguna asignación a esta variable x.


Lo que ha ocurrido en realidad, es que se ha creado una nueva variable x cuyo valor es 4 + 1 = 5 en este scope interno y esta variable solo existe en el scope de la expresion lambda.


Por otra parte, al hacer &r = x hemos asignado una referencia a la variable externa x, que se encuentra fuera del scope de la expresion lambda y por medio de r += 2 si cambia el valor de esta variable externa. Por consiguiente, el valor de la variable externa x cambiara a 25 al ejecutarse la expresion lambda.  


Ejemplos de closures (expresiones lambda)

Con lista de capturas vacia

1
2
3
4
void print_vector(std::vector<int> & vec){
    std::for_each(vec.begin(),vec.end(),[](int val){ std::cout << val << ", ";});
    std::cout<<std::endl;
}

Usando std::for_each podemos iterar sobre todos los elementos en el vector. Aqui lo usamos para imprimir los valores. En este caso la closure recibe el int val como parametro.


Usando algoritmos genericos

1
2
3
4
5
6
7
void lambda_expresion_usando_algoritmos_genericos(){
    std::cout << "# --- lambda_expresion_usando_algoritmos_genericos --- #\n";
    std::vector<int> c = {1, 2, 3, 4, 5, 6, 7};
    int x = 5;
    c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; }), c.end());
    print_vector(c);
}

En este caso usamos la expresion lambda como predicado para la funcion std::remove_if [2], en este caso la lista de captura esta vacia, pero la lista de parametros no. El unico parametro es n el cual representa el valor actual en el vector.


std::remove_if borra todos los elementos que satisfacen ciertos criterios dados por la expresion lambda, tomando en cuenta el rango de valores dado por c.begin() y c.end()


remove_if acepta funciones normales, enunciados o expresiones lambda como predicado, siempre y cuando estas puedan convertirse en un tipo boolean (en el caso de funciones se toma el valor retornado de ellas).


El output en la consola.

Tomando capturas por referencia

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void tomando_capturas_por_referencia(){
    std::cout << "# --- tomando_capturas_por_referencia --- #\n";

    std::vector<int> vec(10,0);
    print_vector(vec);

    int n = 0;
    // con &, el busca las variables que necesita y las manda by reference
    std::generate(vec.begin(), vec.end(), [&](){return n+=2;});    
    print_vector(vec);

}

Esta vez usamos el algoritmo std::generate, el cual asigna el resultado de un predicado o función consecutivamente, a cada elemento en el rango dado por los iteradores vec.begin y vec.end().   


Asi que std::generate ejecutara la expresion lambda cada vez, para cada objeto en el vector y asignara el resultado de ella al objeto actual.


Recordemos que usando & en la lista de capturas, provoca que el compiler busque automaticamente las variables necesarias, que son usadas en el body de la expresion lambda, y las ejecute como referencia.


Por otro lado, las capturas [ ] y [ = ] son equivalentes y llamaran a las variables por copias.


Pasandole parametros a una closure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void lambda_expresion_con_parametros(){
    std::cout << "# --- lamda_expresion_con_parametros --- #\n";
    int x = 4;
    auto var =  2.2;
    auto y = [&r = x, x = x + 1](auto param)
    {
        std::cout << "Parametro: "<< param<< std::endl;
        r += 2;
        return x * x;
    }(var); // Cuando se usa como objeto, los () son necesarios para ejecutar el cuerpo de la función
    std::cout<< "y: " << y << ", x: " << x << std::endl;
}

Al usar una expresion lambda como objeto, podemos usar el operador ( ) para ejecutar el cuerpo de la expresion, al igual que una funcion, y asi entregarle tambien parametros. En este ejemplo le entregamos el parametro var a la expresion en la linea 10.


Output en la consola.

Usando argumentos por defecto

1
2
3
4
5
void lambda_expresion_con_argumentos_default(){
    std::cout << "# --- lambda_expresion_con_argumentos_default --- #\n";
    auto func1 = [](int i = 6) { return i + 4; };
    std::cout << "func1: " << func1() << '\n';
}

Desde C++14, podemos usar argumentos por defecto. Por esta razón podemos escribir int i = 6 en la linea 3 .


Tomando capturas con inicializacion 

1
2
3
4
5
6
7
8
9
void tomando_capturas_con_inicializacion(){
    std::cout << "# --- tomando_capturas_con_inicializacion --- #\n";
    int x = 4;
    auto y = [&r = x, x = x + 1]()->int
    {
        r += 2;
        return x * x;
    }(); 
}

En las capturas podemos hacer algo parecido a tomar un argumento por defecto. Solo que aqui, debemos prestar atención al scope y al metodo de captura ( por referencia o por copia) y le llamamos inicialización.


Tomando capturas con inicialización simplificado

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void tomando_capturas_con_inicializacion_simpleficado(){
    std::cout << "# --- tomando_capturas_con_inicializacion_simpleficado --- #\n";
    int x = 4;
    auto y = [&r = x, x = x + 1]()
    {
        r += 2;
        return x * x;
    }(); 
    std::cout<< "y: " << y << ", x: " << x << std::endl;
}

En este caso podemos simplificar la expresion, al quitar el tipo de regreso y el operador ->. Esto es posible ya que estamos usando la palabra auto para inferir el tipo de dato de la variable entregada por la funcion.


SIgue en la segunda parte de esta publicación aqui: https://thewhitecode.com/blog/5703



Fuentes:

-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-


QuickSort vs MergeSort vs Heapsort

Quieres entender como funcionan estos algoritmos?

Te recomiendo mi publicacion sobre estos aqui https://thewhitecode.com/blog/6097


Descarga el manual de los algoritmos y las estructuras de datos directamente desde este enlace:

https://leanpub.com/elmanualdelosalgoritmosylasestructurasdedatos


-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-


Sanchez

Profesional en informatica medica con enfasis en algoritmos y analizis de imagen en C++. Programador con experiencia en C/C++ y Python.

Reactions

13

1

0

0

Responsive image

20-12-21 14:44

He comprado tu libro y me parece genial, cuando publicaras los ultimos 2 capitulos? Muy buen tutorial.

Access hereTo be able to comment

TheWhiteCode.com is not the creator or owner of the images shown, references are: