Inicio > .NET Framework, Apunte, C#, C++ > Comparando lambdas entre C++ y C#

Comparando lambdas entre C++ y C#


Ahora que me ha dado por explorar las nuevas características de C++ 11, muchas de las cuales han sido ya incorporadas a Visual C++ 10, entre otras cosas me he puesto a explorar los lambdas de C++. Y naturalmente, tras mucho tiempo de trabajar con C# y .NET surge la inquietud/curiosidad de compararlas. Así, comencemos a revisar un poco de historia.

Funciones de retro-llamada (callbacks)

En la era antigua, cuando un programador de C++ quería pasar una función como parámetro, sacaba su cincel y tallaba en piedra un código similar al que sigue:



#include <string>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <list>

using namespace std;

class employee
{
    public:
        employee()
            : _age(0), notify_changes(nullptr)
        {
        }

        employee(const string& name, int age, double salary)
            : _name(name), _age(0), notify_changes(nullptr)
        {
        }

        employee(const employee& copy)
            : _name(copy._name), _age(copy._age), notify_changes(nullptr)
        {
        }

        string name() const { return _name; }
        int age() const { return _age; }
        
        void (*notify_changes)(const string& msg);

        void name(const string& value)
        {
            if (_name != value)
            {
                _name = value;
                if (notify_changes != nullptr)
                {
                    string msg = "Name ha cambiado, nuevo valor: " + value;
                    notify_changes(msg);
                }
            }
        }

        void age(int value)
        {
            if (_age != value)
            {
                _age = value;
                if (notify_changes != nullptr)
                {
                    stringstream msg;
                    msg << "Age ha cambiado, nuevo valor: " << value;
                    notify_changes(msg.str());
                }
            }
        }

    private:
        string _name;
        int _age;
};

void print(const string& msg)
{
    cout << "\nRecibiendo mensaje: " << "\n\t" << msg << endl;
}

int main(int argc, char* argv[])
{
    employee emp;
    emp.notify_changes = &print;

    emp.name("Fernando Gómez. ");
    emp.age(28);

    system("pause");
    return 0;
}

Este código muestra un básico patrón del observador, en el cual una entidad externa al objeto observado recibe “notificaciones” o actualizaciones por parte de éste. En este caso tenemos una clase employee que “notifica” cuando alguna de sus propiedades cambia. Para la notificación se expone un puntero a función (también llamados callbacks o, intentando traducir, función de retro-llamada), llamado notify_changes. Pueden ver la sintaxis que es un poco engorrosa. Así, cuando name o age cambian, invocamos el puntero a la función, si éste no es nulo.

Podemos ver que en main(), es simple cuestión de asignar al puntero una función, en este caso una referencia a print. Tras esto, cualquier invocación a name() o age() hará que print() sea ejecutada.

image

Así pues, esta es la forma en la que hacíamos las cosas con C++ hace años. Por cierto que podemos usar plantillas para que la cosa no quede tan fea. Así fue como muchos elementos de la biblioteca estándar de C++ fueron implementados.

Functores

Como puedes ver del ejemplo anterior, los callbacks proveen ciertas dificultades. De entrada la sintaxis, pero además la falta de poder usar polimorfismo y otras herramientas del lenguaje.

Ante esto, comenzó a desarrollarse un idioma en C++, el cual consiste en sobrecargar el operador () para tratar clases como si fueran funciones. A este tipo de construcción se le llamó un functor. Y son perfectos para utilizarse con plantillas.

class functor
{
    public:
        int a; int b;

        void operator(const string& msg) const
        {
            cout << msg << a + b << endl;
        }
};

...

functor func;
func.a = 5;
func.b = 10;
func("Resultado de suma: ");

El código anterior muestra un functor básico. Dado que es una clase podemos hacer todo lo que hacemos con clases: tener estado, otros métodos y propiedades, atributos, herencia, polimorfismo… Mucho mejor que un simple callback.

Eventos y delegados en .NET

Cuando llega .NET una de las cosas que más recuerdo era la posibilidad de crear callbacks de forma fácil, fuertemente tipados y siendo menos propensos a errores. Esta característica de C# fue la que más llamó mi atención. Aunque, por supuesto, en .NET no se llaman callbacks, sino delegados.

public delegate void NotifyChanges(string msg);
...

public void Print(string msg)
{
    Console.WriteLine("Recibiendo mensaje: " + msg);
}
...

NotifyChanges miCallback = new NotifyChanges(Print);
miCallback("¡Hola mundo!");

Del código anterior la primera línea declara un delegado. Es similar a la declaración de un callback de C++, pero quitando la sintaxis horrible. Luego mostramos un método cualquiera, similar al print anterior, y por último mostramos cómo incializar una variable delegado y hacer que apunte hacia el método Print anterior.

Los eventos son semánticamente diferentes a los delegados, pero técnicamente son prácticamente lo mismo. Para declarar un evento (dentro de una clase o interfaz) necesitamos usar la palabra reservada event. Por lo demás, los invocamos como delegados. Así, podemos replicar el código del empleado anterior en C#.

using System;

public delegate void NotifyFunc(string msg);

public class Employee
{
    public Employee(string name, int age)        
    {
        _name = name;
        _age = age;
    }

    public Employee()
        : this(string.Empty, 0)
    {
    }

    public event NotifyFunc Notify;

    protected virtual void OnNotify(string msg)
    {
        if (Notify != null)
            Notify(msg);
    }

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;
                OnNotify("Name ha cambiado, nuevo valor: " + value);
            }
        }
    }

    public int Age
    {
        get { return _age; }
        set
        {
            if (_age != value)
            {
                _age = value;
                OnNotify("Age ha cambiado, nuevo valor: " + value);
            }
        }
    }

    private string _name;
    private int _age;
}

static class Program
{
    static void Print(string msg)
    {
        Console.WriteLine("Recibiendo mensaje: " + msg);
    }

    static void Main(string[] args)
    {
        Employee emp = new Employee();
        emp.Notify += Print;
        emp.Name = "Fernando Gómez. ";
        emp.Age = 28;

        Console.ReadKey(true);
    }
}

La utilización de delegados se ha extendido mucho en el mundo de C# y .NET. Las interfases gráficas es quizás donde más se emplean. Pero también otros lados. Por ejemplo, la clase List<T> tiene un método Find, que toma un predicado como parámetro, un delegado que regresa true o false, dependiendo de si es el elemento buscado.


bool FindByName(Employe emp)
{
    return emp.Name == "Fernando";
}

List list = new List();
...
Predicate func = new Predicate(FindByName);
Employee found = list.Find(func);

 

Ahora bien, desde hace alguans versiones de .NET exsiten los delegados anónimos. Éstos nos permiten declarar funciones dentro del cuerpo de otro método. Así, el ejemplo anterior podríamos reescribirlo como:

List<Employee> list = new List<Employee>();
...

Employee found = list.Find(new delegate(Employee emp) {
        return emp.Name == "Fernando";
    });

 

Lambdas en .NET

Como hemos podido ver, los delegados ayudan en muchas situaciones, como en la creación de eventos, la implementación de ciertos patrones de diseño y como en situaciones como el List<T>.Find. Sin embargo, la sintaxis no es muy cómoda aun.

Del cálculo lambda, aquella rama de las matemáticas que se dedica a investigar la definición de función, aplicación de funciones y recursión, surge el concepto de funciones lambda. Éstas son, en esencia, declaración de funciones anónimas (en .NET delegados anónimos) utilizando una sintaxis similar. Una función, por ejemplo, indica su parámetro seguido de una flecha, donde se indica el resultado. Así, f(x) = x*x podemos reescribirla como: x –> x*x usando dicha notación.

Pues bien, en C# así se implementan. La siguiente declaración crea un delegado anónimo que toma un número como parámetro y regresa su cuadrado.

Func<int, int> func = x => x * x;

Por supuesto, podemos usar un lambda para crear un delegado que tome como parámetro un objeto de tipo Employee y nos regrese un valor booleano:

var func = x => x.Name = "Fernando";

Ahm… bueno, la verdad es que el código anterior no funciona… Con una declaración así, el compilador de C# no puede saber el tipo de la variable x. Pero podemos cambiarlo de tal  forma que sí lo infiera. Por ejemplo, un delegado de tipo Predicate concuerda con la definición de lo que queremos hacer.

Predicate<Employee> func = x => x.Name == "Fernando";

De esta forma el compilador infiere el tipo del delegado. Funciona igual con los parámetros de un método: gracias a esto podemos usar un lambda directamente en nuestro método Find:

List<Employee> list = new List<Employee>();
...

Employee found = list.Find(x => x.Name == "Fernando");

Mucho mejor, ¿no?

Ahora bien, ¿qué pasa si necesitamos más parámetros o ejecutar más acciones? Pues usamos paréntesis y llaves. Por ejemplo, el método List<T>.Sort pide un delegado que tome dos parámetros de entrada y los compare.

List<Employee> list = new List<Employee>();
...

list.Sort( (x, y) => x.Name.CompareTo(y.Name));

Si quisiéramos usar un lambda que además escriba la comparación en la consola, haríamos algo así:

List<Employee> list = new List<Employee>();
...

list.Sort( (x, y) => {
    Console.WriteLine("Comparando: {0} vs {1}", x.Name, y.Name);
    return x.Name.CompareTo(y.Name);
});

Hay otra característica de las funciones lambdas que no hemos mencionado: la capacidad para acceder a variables que están al alcance. Por ejemplo, regresemos al ejemplo de la búsqueda. Quizás en lugar de comparar contra “Fernando” queremos comparar contra el valor de alguna variable. En ese caso, es legal hacer esto:

List<Employee> list = new List<Employee>();
...

string name = "Fernando";
Employee found = list.Find(x => x.Name == name);

La función lamda está accediendo a una variable que está declarada fuera de su cuerpo. Por supuesto, el compilador CLR, internamente, al crear el delegado anónimo, detecta que la variable externa es utilizada dentro del lambda y genera una función con un parámetro adicional. Dicha variable externa siempre se pasará por referencia. Ésto último podemos probarlo de forma sencilla:

Int32 i = 0;
Action func = () => i++;
func();
Console.WriteLine(i);

Cuando ejecutamos este código, se imprime “1”. Action es un delegado sin parámetros y con tipo de retorno void. Creamos un lambda: () indica que no hay parámetros, y simplemente aumentamos en uno la variable externa. E Int32 es una estructura, que se pasaría por valor en cualquier función normal. No en este caso.

Lambdas en C++

Así, ahora que estamos al borde de la versión 4.5 de .NET, vemos cómo los lambdas de .NET han simplificado el desarrollo, y son en esencia azúcar sintáctica bastante útil. Pues bien, dada su popoularidad (no sólo en .NET, sino también en el mundo de la programación funcional, en lenguajes como OCaml o incluso el reciente F#), hace tiempo que el comité que aprueba el estándar de C++ ha trabajado para incorporar lambdas a este lenguaje. Finalmente se han aceptado y ya están definidos en la versión del estándar C++ de 2011.

Afortunadamente para quienes usamos herramientas Microsoft, nuestro querido y no siempre bien ponderado Visual C++ 2010 ¡ya incluye esta característica! Así que ya podemos usar lambdas en C++. Aunque a fuer de ser sincero, los lambdas de C++ tienen una sintaxis… ehm… compleja, digamos.

Vamos por partes. Para declarar un lambda comenzamos por poner unos corchetes. Acto seguido, entre paréntesis listamos los parámetros como los de cualquier función. Luego, entre llaves, va el cuerpo del método. Algunos ejemplos.

auto func1 = []() { };
func1();

auto func2 = []() { cout << "Hola mundo!" << endl; };
func2(); // imprime el Hola mundo!

auto func3 = [](int i) { return i * i; };
int sqr = func3(5); // regresa 25
cout << sqr << endl;

auto func4 = [](list<int>& list) {
    for (auto i = list.begin(); i != list.end(); ++i)
        cout << *i << endl;
};

auto func5 = [](list<int>& list) {
    for (int i = 1; i <= 10; i++)
        list.push_back(i*i);
};
list<int> l;
func5(l);
func4(l);

En este ejemplo creamos cinco funciones lambdas. Nos apoyamos en la palabra reservada “auto” para determinar el tipo de variable con base en el tipo de la asignación (igual al “var” de C#).

La primera función, func1, no hace nada: no tiene parámetros, no tiene valor de retorno ni haca absolutamente nada: posiblemente el lambda más sencillo que podamos crear. Por su parte, func2 imprime en consola el tradicional “Hola mundo!”. Esta lambda no tiene parámetros ni tipo de retorno (es decir, es “void”), pero sí tiene cuerpo. Con func3 la cosa ya se pone más interesante: toma un parámetro y regresa el cuadrado del mismo.

La lambda func4 está más interesante aún: toma una referencia a una lisat e imprime en consola todos sus elementos. Adicionalmente, func5 toma una lista también y le inserta el cuadrado de los primeros diez números naturales. Luego nos creamos una lista y la pasamos a func5 para que genere los números y luego a func4 para que los imprima.

Ahora bien, te preguntarás: ¿por qué no mejor creamos la lista y que el lambda haga referencia a ésta, sin tener que pasarla como parámetro? ¡Buena idea!

list<int> l;
auto func = [](int min, int max) {
    for (int i = min; i < max; i++)
        l.push_back(i*i);
};
func(1, 10);

Similar a C#, ¿no? Sin embargo, al compilar… ¡tómala! Aparece este mensaje de error:

'l' cannot be implicitly captured because no default capture mode has 
been specified.

Changos. ¿Por qué no compila? Cuando veíamos la versión de C# dijimos que el compilador del CLR pasa en automático las variables que están fuera del lambda. En C++ no se hace en automático: hay que especificarle al compilador cómo queremos pasar las variables externas. Y (por si te lo habías preguntado) aquí es donde entran en juego los corchetes.

En efecto, dentro de los corchetes especificamos qué parámetros externos usaremos, y cómo queremos pasarlos a la función lambda. Hasta ahora hemos dejado corchetes vacíos, que quiere decir: no pases ninguna variable externa. Ahora lo que haremos será añadir un ámperson adentro, lo cual quiere decir: pasa todas las variables por referencia.

list<int> l;
auto func = [&](int min, int max) {
    for (int i = min; i < max; i++)
        l.push_back(i*i);
};
func(1, 10);

Como puedes ver en la línea resaltada, hemos añadido el ámperson. Así, estamos pasando todas las variables externas por referencia, y por ende “l” se pasa así. Ya no tenemos problema.

Ahora bien, supongamos que cambiamos la definición del lambda y tanto min como max serán variables externas. Pero éstas no queremos pasarlas por referencia, sino por valor. Lo que hacemos es enumerar entre los corchetes la lista de parámetros externos, separados por coma, y poner el nombre de la variable en cuestión.

list<int> l;
int min = 1;
int max = 10;
auto func = [&l, min, max]() {
    for (int i = min; i < max; i++)
        l.push_back(i*i);
};
func();

Como puedes ver en el ejemplo, estamos pasando la lista por referencia, mientras que el rango min-max lo pasamos por valor.

Por último, hay que recordar que los lambdas generan un tipo de dato bien definido. Por ello, podemos usarlos incluso como parámetros de plantillas. De esto mismo ya hemos hablado en una entrada anterior.

Comparando

A lo largo de esta entrada hemos visto cómo históricamente ha sido necesario tratar las funciones como si fueran variables. Al principio teníamos callbacks, y luego en el mundo de .NET tuvimos delegados. Luego vimos cómo éstos evolucionaron en delegados anónimos y finalmente en lambdas. Y por último, vimos cómo C++ se puso al corriente en este tema.

Tras este recorrido, me gustaría señalar algunas similitudes y diferencias que he encontrado entre ambas implementaciones. Sería interesante que si conoces alguna que no esté listada, ¡me la hagas llegar para exponerla y platicarla!

0.- La sintaxis de C++ apesta y la de C# es más limpia.

Bueno, al menos con C# podemos usar expresiones compactas cuando necesitamos una sóla línea de código, como al momento de filtrar o comparar en los ejemplos anteriores. Con C++ nunca es el caso. Pero bueno, al final no debería sorprendernos: la sintaxis de C++ suele ser más complicada aunque en ocasiones menos verbosa. C# por su parte fue diseñado para ser un lenguaje much más claro.

1.- Los lambdas en C# siempre se traducen a un delegado, mientras que los lambdas en C++ se traducen a un tipo de dato indefinido.

Esto, por supuesto, no quiere decir que en C++ los lambdas no tengan tipo: claro que lo tienen, tanto que hasta podemos usar typeid de esta forma:

auto func = []() { };
const type_info& type = typeid(func);
cout << type.raw_name() << endl;

/* en mi compilador, lo anterior imprime:

.?AV@?A0xe4d096c9@@
Press any key to continue . . .

*/

La diferencia radica en que en C++ nunca sabremos bien el tipo de dato del lambda, mientras que en C# sí lo sabemos. De hecho, en C# el compilador necesita alguna forma de inferir el tipo de dato, es decir, necesita inferir el delegado en cuestión.

2.- En C++ los lambdas pueden asignarse a variables con tipo de dato implícito, mientras que en C# no puede hacerse.

Esto es un corolario del punto anterior. En efecto, mientras que en C++ podemos inicializar un lambda hacia una variable con el modificador “auto”, hacer algo similar en C# con la palabra “var” nos manda error de compilación.

// C++ - OK
auto func = []() { };
// C# - error CS0815 Cannot assign lambda 
// expression to an implicitly-typed local variable
var func = () => { };

Como decíamos: en C++, el lambda tiene un tipo anónimo y por eso “auto” funciona, mientras que en C# necesariamente tiene que mapearse a un delegado: es por ello que el lambda necesita conocer de antemano el tipo del delegado.

3.- En C# todas las variables externas están disponibles en automático, y en C++ hay que hacerlo explícito.

En efecto, hemos visto cómo en C# podemos referenciar cualquier variable externa y listo, mientras que en C++ tenemos que, o bien nombrarlas explícitamente, o bien especificar que queremos que todo se pase por referencia (con el “[&]”).

4.- En C++ podemos controlar cómo pasar las variables externas.

Como ya decíamos, podemos especificar si una variable externa se pasa por valor o por referencia, o incluso pasar una por valor y otra por referencia.

5.- En ambos casos, las librerías de ambos lenguajes permiten usar lambdas para realizar filtros y acciones sobre datos.

Vimos varios ejemplos, como List<T>.Find o List<T>.Sort. Por el lado de C++, podemos usar std::for_each para ejecutar una acción (el lambda) sobre algún iterador. Por ejemplo:

list<int> l;
for (int i = 1; i <= 10; i++)
	l.push_back(i);

for_each(l.begin(), l.end(), [](int i) { cout << i*i << endl; });

 

Por último…

Bueno, pues eso ha sido todo por hoy. Estaría interesante que pudiésemos seguir analizando los lambdas, así que si quieres comentar algo, no dudes en dejar tu mensaje. ¡Incluso, me gustaría que buscásemos más similitudes y diferencias que las aquí mencionadas!

¡Nos vemos hasta la próxima!

Anuncios
Categorías:.NET Framework, Apunte, C#, C++ Etiquetas: , , ,
  1. Aún no hay comentarios.
  1. febrero 12, 2012 en 2:06 am

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s