Funciones lambda en C++0x
Una de las funcionalidades menos esperadas, pero en mi opinión más bienvenidas, del nuevo estándar de C++ son las funciones Lambda. Originalmente creadas para los lenguajes funcionales, cada vez encuentran más uso en los lenguajes habituales (reconozcámoslo, los lenguajes funcionales no se usan mucho), véanse los delegates en Java y C#, por ejemplo. La inclusión de este concepto en C++, aunque algo limitada en comparación con otros lenguajes de más alto nivel, ofrece muchas posibilidades interesantes.
Introducción
Pero ¿qué es una función lambda? Simplificando: es una expresión tal que su evaluación resulta en una función. Técnicamente, la expresión es una expresión lambda y el resultado de su evaluación es una función lambda. La función se guarda en una variable (o su equivalente en el lenguaje elegido) y puede llamarse tantas veces como sea necesario.
Un ejemplo fácil de entender es el de Python:
>>> def Multiplicador(a): ... return lambda x : a * x ... >>> doble = Multiplicador(2) >>> doble(3) 6 >>> triple = Multiplicador(3) >>> triple(10) 30
En Python las funciones lambda se limitan a una única expresión y el return se sobreentiende. Esto es así sobre todo por la sintaxis del lenguaje: los bloques de código deben estar indentados pero las expresiones deben ir en una única línea.
El problema para implementar lambdas en un lenguaje compilado es doble:
- La evaluación de la expresión lambda crea una función nueva. Pero en estos lenguajes las funciones se crean en compilación, no en ejecución.
- Para que sean realmente útiles las funciones lambda deben poder tener acceso a las variables existentes en su entorno. En el ejemplo de Python anterior la función lambda puede acceder a la variable a. El conjunto de expresiones así accesibles se llama closure (traducido a veces como clausura, cierre o captura) de la función lambda.
Lambdas en C++
En C++ se resuelven estos problemas haciendo que las expresiones lambda creen, no una función, sino un functor, es decir un objeto de un tipo generado que sobrecarga el operator() y por lo tanto pueda usarse como función. Las expresiones escogidas para el closure se convierten en variables miembro del functor.
La sintaxis es curiosa pero sencilla. Veámosla con un ejemplo:
auto doble = [](int x) -> int { return 2 * x; };
Los corchetes introducen la expresión lambda, luego vienen los argumentos de la función entre paréntesis; después una flecha y el tipo de retorno; y finalmente el cuerpo de la función entre llaves, tan larga como sea necesaria. Y no olvides que estamos en mitad de una expresión, por lo que hay que terminarla con el ; (de hecho es el único caso de todo el lenguaje en el que se anida una sentencia dentro de una expresión).
Puesto que el tipo de la función lambda es una clase inventada por el compilador, la única forma de capturar el valor es o bien una variable de tipo automático (como en el ejemplo) o como argumento genérico de un template. Los candidatos obvios son los algoritmos de la librería estándar:
std::vector<int> lista{1, 2, 3}; std::transform(lista.begin(), lista.end(), lista.begin(), [](int x) -> int { return 2 * x; });
NOTA: No, no he olvidado los paréntesis en el constructor de lista. Es la nueva sintaxis para listas de inicialización. Pero eso es otra historia…
Naturalmente, la función lambda puede hacerse tan complicada como sea necesario:
std::vector<int> lista{1, 2, 3}; std::transform(lista.begin(), lista.end(), lista.begin(), [](int x) -> int { switch (x % 3) { case 0: return 0; case 1: return x; case 2: return -x; } } );
Esta nueva sintaxis seguramente cree un interminable debate sobre la mejor forma de indentarla (personalmente, todavía no me he decidido, siempre me queda algún ; o ) que no sé dónde colocar). Y los editores y coloreadores de sintaxis se vuelven un poco confusos.
A estas alturas seguramente estarás pensando que esos corchetes deberían llevar algo dentro. Y efectivamente, dentro de ellos se escribe una lista (posiblemente vacía) de nombres que forman la lista de captura, es decir el closure. Esta lista no puede contener expresiones arbitrarias, solamente nombres de variables y this (si está dentro de una función miembro de una clase, claro). Eso sí, se puede anteponer un & antes de un nombre para indicar que en el closure se guarde una referencia a la variable, y no una copia.
Así el ejemplo anterior se puede generalizar:
std::vector<int> lista{1, 2, 3}; int factor = ... ; std::transform(lista.begin(), lista.end(), lista.begin(), [factor](int x) -> int { return factor * x; });
Si te sientes particularmente perezoso y la lista de captura es muy larga, en lugar de listar las variables expresamente se puede indicar simplemente = para hacer la lista por copia o & para hacerla por referencia:
std::vector<int> lista{1,2,3}; int suma = 0; std::for_each(lista.begin(), lista.end(), [&](int x) { suma += x; }); std::cout << "Total " << suma << std::endl;
Aunque personalmente recomiendo usar siempre la lista completa, no sea que captures lo que no debes sin darte cuenta.
Y, aunque obvio, es importante resaltar que si se captura una expresión por referencia, el objeto original se destruye y después se llama a la función lambda, es más que probable que se produzca comportamiento indefinido.
Opciones avanzadas
En el caso de lambdas sencillas, que consistan en un simple return expr; no es necesario indicar el tipo de retorno pues se deduce automáticamente del tipo de la expresión (de forma similar a las variables auto):
auto doble = [](int x) { return 2 * x; });
Si el tipo de retorno es void, no es necesario indicarlo en ningún caso. Y si la función lambda no tiene parámetros no es necesario escribir los paréntesis vaciós. Así que la función lambda más sencilla, que no hace nada, es []{}.
Por defecto, los valores capturados como copia (pero no como referencia) por una función lambda no pueden modificarse dentro de la propia función (el operator() sobrecargado es const). Si se añade la palabra reservada mutable se elimina esta restricción, lo que permite construcciones interesantes:
std::vector<int> lista{1,2,3}; int prev = 0; std::transform(lista.begin(), lista.end(), lista.begin(), [prev](int x) mutable ->int { int temp = prev; prev = x; return temp + x; } );
Si la captura se hace por referencia no es necesario indicar mutable, pero entonces tienes que asegurarte de que la variable capturada existe durante toda la vida de la función lambda.
Las funciones lambda pueden anidarse, e incluso una lambda puede ser capturada por otra, pero lo cierto es que no se me ocurre ningún ejemplo en el que esto pueda ser remotamente útil. Así que ahí va un ejemplo inútil:
std::vector<int> lista1{1,2,3}, lista2{10,20,30}; int suma = 0; std::transform(lista1.begin(), lista1.end(), lista1.begin(), [&lista2](int x) -> int { std::for_each(lista2.begin(), lista2.end(), [&x](int y) { x *= y; }); return x; } );
Está claro que las funciones lambda pueden hacer nuestro código más breve, más expresivo, pero mucho más difícil de entender, ¡al más puro estilo de la programación funcinal!
Funciones lambda y std::function
Si queremos hacer una función similar a la del primer ejemplo de Python tenemos un problema: no podemos escribir una función que devuelva una lambda porque el tipo de una función lambda no tiene nombre. Si piensas usar decltype sobre la lambda, olvídalo, porque está prohibido.
Podemos resolverlo con un rodeo: escribimos una clase template que encapsule la lambda, y la llamamos usando una función virtual. Algo más o menos así:
struct IntToInt { virtual ~IntToInt() { } virtual int operator()(int x) = 0; }; template <typename T> class IntToIntLambda : public IntToInt { public: IntToIntLambda(T lambda) :m_lambda(lambda) { } virtual int operator()(int x) { return m_lambda(x); } private: T m_lambda; }; template<typename T> IntToIntLambda<T> *CreateIntToInt(T lambda) { return new IntToIntLambda<T>(lambda); } IntToInt *Multiplicador(int a) { return CreateIntToInt([a] (int x) { return a * x; }); } int main() { auto *doble = Multiplicador(2); std::cout << (*doble)(3) << std::endl; delete doble; return 0; }
Funciona, pero es tremendamente engorroso, además de feo (sobre todo el delete del final), y la idea de las funciones lambda es que sean breves y bonitas.
Afortunadamente la librería estándar nos proporciona una clase template que resuelve exactamente este problema (y muchos otros): std::function.
Con ayuda de este template, origindado en la siempre excelente boost, el ejemplo queda así:
#include <functional> #include <iostream> std::function<int(int)> Multiplicador(int a) { return [a](int x) { return a * x; }; } int main() { auto doble = Multiplicador(2); std::cout << doble(3) << std::endl; auto triple = Multiplicador(3); std::cout << triple(10) << std::endl; return 0; }
Y ahora sí, ¡este código es tan elegante como el de Python!
Editado el 14 de mayo de 2010.
Acabo de repasar el nuevo borrador de C++0x, y han añadido una funcionalidad interesante: si la función lambda no captura ninguna variable, entonces es convertible a un puntero a función ordinaria con el prototipo correspondiente. Esto permite usar lambdas sin closure en APIs de C.
Por ejemplo, un timer de GTK+/GLIB:
gtk_timeout_add(1000,
[](gpointer data) -> gboolean
{
std::cout << "Tick" << std::endl;
return TRUE;
},
NULL);
Por ejemplo, un timer de GTK+/GLIB:
gtk_timeout_add(1000, [](gpointer data) -> gboolean { std::cout << "Tick" << std::endl; return TRUE; }, NULL);
Artículos relacionados:
- Deducción de tipos en C++0x con decltype El otro día veíamos la deducción automática de tipos usando...
- Tipos automáticos en C++0x con auto La palabra reservada auto es casi con toda seguridad la...
- Bucle foreach en C++0x A menudo se dice que C++ es un lenguaje complicado,...
- Templates con número variable de argumentos en C++0x Allá por el año 1999 se publicaba el nuevo y...
RSS
Deja un comentario