Inicio > Apunte, C++, Independiente > Functores y objetos llamables en la librería estándar de C++

Functores y objetos llamables en la librería estándar de C++


Hace algunos días escribí acerca de los lambdas de C++ y C#. En esa oportunidad hice un recuento sobre las opciones que en C++ existen para tratar funciones como parámetros, o en general, poder tener “objetos llamables” como variables. En particular, exploramos tres alternativas:

1.- Funciones de retro-llamada. También llamados “callbacks”, estas funciones son los famosos punteros a void* (o al tipo de dato que retornan).

void print(const string& str)
{
    cout << str << endl;
}

void foo()
{
    void (*func)(const string& str);
    func = &print;
    func("Hola mundo!");
}

2.- Functores. Estas son clases que implementan el operador (), de tal suerte que simulan ser funciones, aunque en realidad sean clases.

class printer
{
    public:
        string str;

        printer() { };
        printer(const string& msg) : str(msg) { }

        void operator()() { 
            cout <str <str = msg;
            cout <str << endl;
        }
};

int main()
{
    printer func;
    func("Hola mundo!");

    func.str = "Hallo Welt!";
    func();

    return 0;
}

3.- Lambdas. Como comentamos, éstas son funciones anónimas cuyo cuerpo se define al momento, i.e. dentro de otra función.

auto func = [](const string& str) {
    cout << str << endl;
};

func("Hola mundo!");

Pues bien, hay una opción de la que no he hablado. En esencia no se diferencia mucho de un functor. O, mejor dicho, es un functor. O aún mejor dicho, es una plantilla que define un objeto que puede ser llamado, como una función o functor. ¡Argh! Me refiero a la clase function, definida en <functional>.

Esta clase es una plantilla cuyo parámetro es –escúchalo bien- la definición de una funcion. Es decir, su firma. Y obvio con esa firma es con la que se define el operador (). Por cierto, el operador de asignación está sobrecargado para recibir la función a ejecutar. Así es, a function de todas formas le tienes que pasar una función como parámetro. Veamos.

#include <iostream>
#include <functional>

using std::cout;
using std::endl;
using std::function;

int sqr(int a)
{
    return a * a;
}

int main(int argc, char* argv[])
{
    function f;
    f = sqr;
    cout << f(5) << endl;

    system("pause");
    return 0;
}

Obvio se imprime “25” en pantalla. En la primera línea definimos nuestra función con parámetro de plantilla int(int). Esto quiere decir que la función tendrá un parámetro de tipo entero y que regresa otro entero. La segunda línea asigna la función sqr (que cumple con la firma de la plantilla). La tercera línea simplemente invoca la función.

Si creas una función, no le asignas la referencia e intentas invocarla, te lanzará una excepción de tipo bad_function_call.

try
{
    function<int(int)> f;
    int a = f(5); // tsssss!
    cout << a << endl;
}
catch (const std::bad_function_call& ex)
{
    cout << ex.what() << endl;
}

Lo mejor: ¡también podemos usar lambdas!

typedef function<int(int, int)> bin_function;

bin_function suma;
suma = [](int a, int b) { return a + b; };

bin_function resta;
resta = [](int a, int b) { return a - b; };

cout << suma(31, 11) << endl;
cout << resta(67, 25) << endl;

Ahora bien, ¿qué ventaja tiene function<T> sobre usar lambdas? Pues que por un lado hace innecesario tener que crear nuestros propios functores de nuevo, y por otro lado, podemos usarlo como parámetro. Por ejemplo, para crear eventos o una implementación del patrón “observer”.

Veamos. Primero nos creamos una clase sencilla: empleado, la cual tiene tres miembros: para nombre, edad y domicilio. Privados. Luego creamos un par de métodos para cada una de éstas: uno que obtiene el dato y otro que lo escribe. Lo que queremos es que cada vez que alguna de estas tres propiedades se actualice, si el dato es diferente, entonces así lo notifique a quien quiera “escucharlo”. Al final, es es lo que hace el patrón de diseño observer. En fin, para ello creamos un método al cual se le llamará cuando cambien las propiedades. Algo así.

class employee
{
    public:
        employee() { }

        string name() const { return _name; }
        string address() const { return _address; }
        int age() const { return _age; }

        void name(const string& value) {
            if (value != _name) {
                _name = value;
                on_property_changed("name");
            }
        }

        void address(const string& value) {
            if (value != _address) {
                _address = value;
                on_property_changed("address");
            }
        }

        void age(int value) {
            if (value != _age) {
                _age = value;
                on_property_changed("age");
            }
        }
    protected:
        void on_property_changed(const string& property_name)
        {
        }

    private:
        string _name;
        string _address;
        int _age;
};

Pero ¿qué ponemos en on_property_changed? Lo que queremos es avisar que el objeto ha sido actualizado, así que esta función debería llamar a los “escuchas” o “listeners” pasándole una referencia a sí mismo. Luego, queremos pasar el nombre de la propiedad que ha cambiado. Para todo esto, quizás sea mejor crear una clase: la llamaremos event_args.

class event_args
{
    public:
        event_args(const employee& sender, const string& property_name)
            : _sender(sender), _property_name(property_name)
        {
        }

        const employee& sender() const { return _sender; }
        string property_name() const { return _property_name; }

    private:
        const employee& _sender;
        string _property_name;
};

Y ahora, nos basta un par de typedefs:  uno que defina una función (std::function<T>, recuerda) que tome como parámetro un objeto de tipo event_args, y luego, un contenedor de este tipo de funciones, como –digamos- std::list.

typedef function<void(event_args&)> event_function;
typedef list<event_function> event_listeners;

Ahora en la clase employee nos creamos un miembro (público, por facilidad) de tipo event_listeners…

event_listeners listeners;

…y procedemos a escribir, ahora sí, nuestra función on_property_changed, que recorra cada función de la lista y la invoque. Easy peasy.

void on_property_changed(const string& property_name)
{
    event_args args(*this, property_name);
            
    auto callback = [&args](event_function& func) { func(args); };
    for_each(listeners.begin(), listeners.end(), callback);
}

Bueno bueno, usamos un lambda y la función std::for_each, pero meh, es lo mismo. Y con esto ya tenemos listo nuestro patrón “observer”, y sólo resta el usarlo: primero creamos una función que imprime el nombre de la propiedad que ha sido modificada. Luego, hacemos otra función que haga ciertas validaciones (nombre no vacío, edad mayor de 16 años, etc.) e imprima un mensaje si no se cumplen. Luego instanciamos nuestro objeto employee y añadiremos las funcinoes a la propiedad listeners.

int main(int argc, char* argv[])
{
    auto print_func = [](event_args& args) { 
        cout << "La propiedad [" 
             << args.property_name()
             << "] ha cambiado. "
             << endl;
    };

    auto validate_func = [](event_args& args) {
        auto sender = args.sender();
        if (sender.name().length() <= 0)
            cout << "El nombre no puede estar vacío." << endl;
        if (sender.age() < 16)
            cout << "La edad mínima laboral es de 16 años." <= 255)
            cout << "La dirección es muy larga." << endl;
        cout << endl;
    };

    employee emp;
    emp.listeners.push_back(print_func);
    emp.listeners.push_back(validate_func);
    emp.name("Fernando");
    emp.name("");
    emp.name("Fernando Gómez");
    emp.age(29);
    emp.address("Algún lugar en la Ciudad de México.");

    system("pause");
    return 0;
}

Al ejecutarse, este programa tiene esta salida:

La propiedad [name] ha cambiado.
La edad mφnima laboral es de 16 a±os.

La propiedad [name] ha cambiado.
El nombre no puede estar vacφo.
La edad mφnima laboral es de 16 a±os.

La propiedad [name] ha cambiado.
La edad mφnima laboral es de 16 a±os.

La propiedad [age] ha cambiado.

La propiedad [address] ha cambiado.

Press any key to continue . . .

¡Así sí gana la gente!

Hemos visto en el pasado cómo usar diferentes técnicas para llamar funciones como variables o parámetros. Y ahora hemos visto cómo podemos combinarlas, junto con esta nueva función exclusiva de C++ 11, para lograr estructuras más complejas. No es que antes no pudieran hacerse, pero ahora no tenemos que escribir tanto código. Y al final de todo es es lo que debe proveer una buena librería, ¿o no?

Por último, vale la pena comentar que esto es apenas un vistazo a std::function y, sobre todo, a todos los objetos y funciones contenidos dentro del archivo <functional>. Poco a poco iremos explorando más posibilidades, así que ¡sigue en contacto!

¡Hasta la próxima!

  1. fran
    marzo 29, 2012 a las 3:47 am

    muy bien explicado, gracias

  1. No trackbacks yet.

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