examples - std c++
Lambda sobre Lambda en C++ 14 (6)
¿Cómo termina / finaliza la siguiente llamada recursiva lambda?
#include <cstdio>
auto terminal = [](auto term) // <---------+
{ // |
return [=] (auto func) // | ???
{ // |
return terminal(func(term)); // >---------+
};
};
auto main() -> int
{
auto hello =[](auto s){ fprintf(s,"Hello/n"); return s; };
auto world =[](auto s){ fprintf(s,"World/n"); return s; };
terminal(stdout)
(hello)
(world) ;
return 0;
}
¿Qué me estoy perdiendo por aquí?
-
terminal (stdout) devuelve una función, llamémosla función
x
, con paramfunc
. Entonces:terminal(stdout) ==> x(func) { return terminal(func(stdout)) };
-
Ahora la terminal (stdout) (hola) llama a la función
x(hello)
:terminal(stdout)(hello) ==> x(hello) { return terminal(hello(stdout)) };
Esto hace que se llame a la función
hello
y devuelve la funciónx
nuevamente. -
Ahora la terminal (std) (hola) (world) llama a la función
x(world)
:terminal(stdout)(hello) ==> x(world) { return terminal(world(stdout)) };
Esto hace que se llame a la función
world
y devuelve la funciónx
nuevamente. La funciónx
ahora ya no se llama ya que no hay más param.
Creo que la fuente de confusión proviene de leer una declaración lambda como una llamada lambda. De hecho aquí:
auto terminal = [](auto term) // <---------+
{ // |
return [=] (auto func) // | ???
{ // |
return terminal(func(term)); // >---------+
};
};
el autor acaba de declarar un
terminal
lambda que toma un
term
argumento arbitrario y devuelve un lambda sin nombre, ¡nada más!
Veamos esta lambda sin nombre:
-
acepta un
func
objeto invocable como argumento y lo llama en elterm
parámetro capturado por copia y -
devuelve el resultado de la terminal llamada con el resultado de la llamada
func(term)
; por lo tanto, devuelve otra lambda sin nombre que captura el resultado defunc(term)
, pero esta lambda no se llama ahora, no hay recursividad.
Ahora el truco en lo principal debería ser más claro:
-
terminal(stdout)
devuelve un lambda sin nombre que ha capturado stdout. -
(hello)
llama a este lambda sin nombre que pasa como arg el hola invocable. Esto se llama en el stdout previamente capturado.hello(stdout)
devuelve nuevamente stdout, que se utiliza como argumento de una llamada a la terminal, devolviendo otra lambda sin nombre que ha capturado stdout. -
(world)
igual que 2.
La clave aquí es entender que esto es válido:
world(hello(stdout));
e imprimirá "Hola Mundo". La serie recursiva de lambdas se puede desenrollar como
#include <cstdio>
auto terminal = [](auto term) // <---------+
{ // |
return [=] (auto func) // | ???
{ // |
return terminal(func(term)); // >---------+
};
};
/*
terminal(stdout) -returns> anonymous_lambda which captures stdout (functor)
anonymous_lambda(hello) is called, func(term) is hello(stdout) and prints "Hello" and returns stdout, the anonymous_lambda -returns> terminal(stdout)
(the above 2 lines start again)
terminal(stdout) is called and -returns> anonymous_lambda which captures stdout (functor)
anonymous_lambda(world) is called, func(term) is world(stdout) and prints "World" and returns stdout, the anonymous_lambda -returns> terminal(stdout)
terminal(stdout) is called and -returns> anonymous_lambda which captures stdout (functor)
nobody uses that anonymous_lambda.. end.
*/
auto main() -> int
{
auto hello =[](auto s){ fprintf(s,"Hello/n"); return s; };
auto world =[](auto s){ fprintf(s,"World/n"); return s; };
world(hello(stdout));
terminal(stdout)
(hello)
(world) ;
return 0;
}
La llamada en sí no es recursiva.
Devuelve un objeto de función que, si se llama, volverá a llamar al
terminal
para generar otro objeto de función.
Entonces
terminal(stdout)
devuelve un functor que captura
stdout
y se puede llamar con otro objeto de función.
Al volver a llamarlo
(hello)
, se llama al functor
hello
con el término
stdout
capturado y se muestra
"Hello"
;
el
terminal
llamadas y devuelve otro functor que esta vez captura el valor de retorno de
hello
, que todavía es
stdout
.
Llamando a ese functor,
(world)
, de nuevo igual, dando como resultado
"World"
.
No es una llamada de función recursiva, mírela paso a paso:
-
terminal(stdout)
: esto simplemente devuelve un lambda que ha capturadostdout
-
El resultado de 1. se llama con el lambda
hello
, que ejecuta lambda (func(term)
), cuyo resultado se pasa aterminal()
, que simplemente devuelve un lambda como en 1. -
El resultado de 2. se llama con el
world
lambda, que hace lo mismo que 2, esta vez el valor de retorno se descarta ...
Se puede traducir internamente en algo que se ve de la siguiente manera:
#include <cstdio>
template <typename T>
struct unnamed_lambda
{
unnamed_lambda(T term) : captured_term(term) {}
template <typename A>
unnamed_lambda operator()(A func);
T captured_term;
};
struct terminal_lambda
{
template <typename A>
unnamed_lambda<A> operator()(A term)
{
return unnamed_lambda<A>{term};
}
};
terminal_lambda terminal;
template <typename T>
template <typename A>
unnamed_lambda<T> unnamed_lambda<T>::operator()(A func)
{
return terminal(func(captured_term));
}
struct Hello
{
FILE* operator()(FILE* s)
{
fprintf(s, "Hello/n");
return s;
}
};
struct World
{
FILE* operator()(FILE* s)
{
fprintf(s, "World/n");
return s;
}
};
int main()
{
Hello hello;
World world;
unnamed_lambda<FILE*> l1 = terminal(stdout);
unnamed_lambda<FILE*> l2 = l1(hello);
unnamed_lambda<FILE*> l3 = l2(world);
// same as:
terminal(stdout)(hello)(world);
}
En realidad, esto es lo que hace el compilador detrás de escena con lambdas (con alguna aproximación).