web analytics

GCC y C++0x: Por fin funciones (lambda) anónimas

Sí: Ciencia, explicada también trata de las computer sciences, aunque yo creo más bien que la programación en concreto es un arte 😉

Hoy voy a explicar cómo usar una de las novedades más esperadas en el mundo del lenguaje C++ con la llegada del estándar C++0x: las funciones (y clases) lambda o anónimas. Estas son funciones definidas in situ, al estilo de lo que siempre se ha podido hacer con Java. Tienen especial utilidad en combinación con la STL, como veremos.

Aunque a día de hoy GCC no implementa de forma completa el estándar C++0x (ningún otro compilador lo hace aún), en esta página del proyecto se mantiene una lista actualizada con el estado de esta transición.

Paso previo: ¡Usar una versión de compilador que soporte C++0x!

Las versiones de GCC que actualmente vienen con muchas distribuciones de Linux son la 4.3, en el que apenas hay características del C++0x.
Para comprobar que versión tienes, ejecuta:


$ gcc --version
gcc (GCC) 4.4.1

Si tienes la 4.4 o superior, ¡perfecto!, saltate la siguiente sección.
Pero lo más probable es que hasta dentro de varios meses no sea el caso, así que voy a explicar como instalarlo a mano:

Instalación de GCC 4.4 con soporte para Lambdas

Lo primero es instalar las dependencias para compilar GCC, más el programa Subversion, lo que se hace en Debian/Ubuntu con:

$ sudo apt-get install build-essential \
libmpfr-dev libgmp3-dev subversion

Ahora nos descargamos los fuentes de la única branch de GCC que actualmente implementa las funciones Lambda (quizás en el futuro haya muchas más branchs, o incluso tags que las implementen):


$ svn co svn://gcc.gnu.org/svn/gcc/branches/cxx0x-lambdas-branch/ gcc-lambdas
$ cd gcc-lambdas

Ahora, si tienes un Linux de 64bits:

$ CFLAGS=-m64 ./configure --disable-multilib

, sino:

$ ./configure --disable-multilib

Y ya a compilar e instalar (puede tardar horas):

$ make -j4
$ sudo make install && sudo ldconfig

¿Como verificar que tu compilador soporta C++0x?

Tanto si ya tenías o acabas de instalar el compilador GCC 4.4 o superior, vamos a probar que efectivamente soporta C++0x. Para eso, copia y pega el siguiente programa de ejemplo en un fichero llamado prueba.cpp:


#include
#include

using namespace std;

int main(int argc,char** args)
{
// Ejemplo del uso de "auto" con iteradores:
map my_map;
my_map[2] = 4.0;
my_map[3] = 9.0;

for (map::iterator it=my_map.begin();it!=my_map.end();it++)
cout << it->first << " -> " << it->second << endl;

for (auto it=my_map.begin();it!=my_map.end();it++)
cout << it->first << " -> " << it->second << endl;

return 0;
}

Y ejecuta:


$ g++ -std=c++0x -o prueba prueba.cpp
$ ./prueba
Si todo va bien, debería compilar y ejecutar sin problemas.

En este ejemplo de arriba he aprovechado para demostrar otra de las más importantes novedades de C++0x: las variables de tipo auto. El compilador detecta automáticamente el tipo que se espera que tengan y nos ahorra teclear bastante, como se ve en el ejemplo (comparar los dos bucles «for»).

Eso sí, no hay que confundir las variables auto con las del tipo variant que existen en otros contextos, ya que siguen siendo fuertemente tipadas y no se puede cambiar su tipo una vez establecido en su declaración.

Funciones lambda y closures

Vamos a ver con unos cuantos ejemplos los usos correctos (e incorrectos) de las funciones lambda, y de sus closures o conjunto de variables que se «importan» desde el contexto donde se crea la función.

  • Variables «puntero» a funciones lambda: La variable «fun1» se comporta como un puntero a función (o functor), solo que no apunta a ninguna función con nombre:

auto fun1 = [](int x,int y)
-> double { return 0.5*(x+y); };
cout << fun1(1,2) << endl; // Salida: 1.5
  • Una de las grandes utilidades de las funciones lambda en C++ es poder usarlas de argumentos en lugar de funciones «normales» o clases que implementen el operator (). En el siguiente ejemplo se ve como sería este uso:

vector numeros;
numeros.push_back(2);
numeros.push_back(3);

int total=0;
int factor = 3;
for_each(
numeros.begin(),
numeros.end(),
[&total,factor](int n) {total += n*factor;});
cout << total << endl; // Salida: 15

  • Lo que va entre los corchetes […] es la closure de la función lambda, y define qué variables serán visibles dentro de la lambda. En este ejemplo se pasan las variables «total» por referencia (entrada/salida) y «factor» como solo lectura (entrada):

// "total" (por referencia), "factor" (por valor)
// Compila OK.
[&total,factor](int n) {total += n*factor;};
  • Es un error acceder a variables que NO se hayan declarado en el closure:

// Error: «factor» no está declarado

[&total](int n) {total += n*factor;};
  • El valor especial [&] significa que todas las variables pasan como referencia (entrada/salida):

// Ok: Todas las variables pasan como referencia
[&](int n) {total += n*factor;};

  • Y el valor especial [=] indica que todas las variables pasan por valor, siendo de solo lectura. Además, realmente se hace una copia de todas ellas, por lo que en el caso de objetos se pueden seguir utilizando aunque el objeto original haya sido destruido:

// Todas las variables pasan por valor
// Error, se asigna a "total" que es de solo lectura
[=](int n) {total += n*factor;};

  • Por último, se puede añadir un «-> tipo» tras el closure para forzar el tipo de salida de la función, aunque esto es opcional si no hay ambigüedades dada la sentencia return. Esta característica es más importante si la salida es de clases creadas por el usuario, existen polimorfismos, etc…

// Declaración explícita del tipo de salida, para evitar ambigüedades:
[&](int n) -> long {total += n*factor;};

Y eso es todo… espero que os haya sido útil este resumen.

Puede que también te guste...