Inicio > Apunte, C++ > Filtrando colecciones con functores y lambdas

Filtrando colecciones con functores y lambdas


La librería estándar de C++ nos provee varias clases que nos permiten almacenar datos. Por ejemplo, std::list para listas, std::map para diccionarios, std::vector para elementos contíguos, etcétera. Todas estas clases de colecciones, también llamadas contenedores, están diseñadas para que su contenido pueda ser accedido mediante clases que enumeran cada elemento. A éstas se les llama iteradores. Los métodos begin y end nos proveen el primer y último iterador. Así, cualquier contenedor puede recorrerse de manera similar al siguiente ejemplo:

list<int> nums;
...

for (list<int>::iterator i = nums.begin(); i != nums.end(); ++i)
{
    cout << *i << endl;
}

O también podemos usar la nueva palabra reservada "auto":

list<int> nums;
...

for (auto i = nums.begin(); i != nums.end(); ++i)
{
    cout << *i << endl;
}

Por otra parte, recorrer un contenedor suele ser tan común que la librería estándar nos propone una función que toma tres parámetros: el iterador de inicio, el iterador de fin, y como tercer parámetro “algo” que tome un parámetro y ejecute una acción. Digo “algo” porque dado que es una plantilla, puede ser una función o, mejor aún, un functor. Un functor, antes que preguntes, es una clase que sobreescribe el operador () de tal suerte que puede invocarse como si fuera una función.

Para ejemplificar estos conceptos, me gustaría antes proponer una clase. Supongamos que vamos a trabajar con empleados. Creamos una clase similar a ésta.

class employee
{
    public:
        enum sex { sexMale, sexFemale };

        employee()
        {
            _age = 0;
            _gender = sexMale;
        }

        employee(const employee& copy)
        {
            _name = copy._name;
            _age = copy._age;
            _gender = copy._gender;
        }

        employee(wstring name, sex gender, int age)
        {
            _name = name;
            _age = age;
            _gender = gender;
        }

        wstring _name;
        sex _gender;
        int _age;
        
        bool operator== (const employee& employee)
        {
            return _name == employee._name;
        }

        bool operator< (const employee& employee)
        {
            return _name < employee._name;
        }
};

Ahora, en nuestra función main, supongamos que creamos un contenedor de esta forma.

employee emp1(L"Fernando", employee::sexMale, 28);
employee emp2(L"Catalina", employee::sexFemale, 31);
employee emp3(L"Moisés", employee::sexMale, 31);
employee emp4(L"Emmanuel", employee::sexMale, 27);
employee emp5(L"Paula", employee::sexFemale, 18);
employee emp6(L"Pedro", employee::sexMale, 24);

list<employee> employees;
employees.push_back(emp1);
employees.push_back(emp2);
employees.push_back(emp3);
employees.push_back(emp4);
employees.push_back(emp5);
employees.push_back(emp6);

Bueno bueno. Luego entonces, decíamos que podemos usar functores. Por ejemplo, creemos uno para imprimir todo el contenido de la lista.

class print_employee
{
    public:
        print_employee()
        {
            _print_all = false;
        }

        print_employee(bool print_all)
        {
            _print_all = print_all;
        }

        void operator()(const employee& employee)
        {
            wcout << employee._name << endl;
            if (_print_all)
            {
                wcout << L"Gender: "  
                    << (employee._gender == employee::sexMale ? L"Male" : L"Female") 
                    << endl;
                wcout << L"Age: " << employee._age << endl;
                wcout << L"*****\n" << endl;
            }
        }

        bool _print_all;
};

 

Ahora sí, usar la función for_each de esta forma.

print_employee print(false);
for_each(employees.begin(), employees.end(), print);

Como puedes ver, print_employee es una clase functor: ve cómo sobrecargamos el operador (). De hecho, podrías invocar directamente print(empleado), como si fuera una función; eso es justamente lo que hace for_each.

Ahora bien, una de las tareas que frecuentemente tendremos que hacer es filtrar un contenedor. Por ejemplo, podríamos querer filtrar los empleados por género, por edad o por nombre. Pues bien, lo que haremos será crear una clase con un functor. Esta clase tendrá un constructor que tomará como parámetro una referencia hacia el contenedor donde guardaremos los empleados que cumplan la condición del filtro. Veamos.

class filter_employee
{
    public:
        filter_employee(list& results)
            : _results(results)
        {
            _by_age = false;
            _min_age = 0;
            _max_age = 0;
            _by_name = false;
        }

        void by_age(int min, int max)
        {
            _by_age = true;
            _min_age = min;
            _max_age = max;
        }

        void by_name(const wstring& name)
        {
            _by_name = true;
            _name = name;
        }

        void operator()(const employee& value)
        {
            bool found = false;
            if (_by_age && !found)
            {
                found = value._age >= _min_age && 
                        value._age <= _max_age;
            }

            if (_by_name && !found)
            {
                found = value._name.find(_name) != wstring::npos;
            }

            if (found)
                _results.push_back(value);
        }

        list& _results;

    private:
        bool _by_age;
        int _min_age;
        int _max_age;
        bool _by_name;
        wstring _name;
};

Si invocamos by_age entonces el functor filtrará por un rango de edades. Si invocamos by_name, entonces buscará una subcadena de texto. Ahora, podemos usar la clase así, para filtrar empleados entre 28 y 42 años:

list results;

filter_employee filter(results);
filter.by_age(28, 42);
for_each(employees.begin(), employees.end(), filter);
for_each(results.begin(), results.end(), print);

Del código anterior, la primera línea inicializa una lista de empleados, donde el functor guardará los resultados obtenidos. La tercera línea inicializa el functor y la cuarta establece que se filtre por edad entre 28 y 42. La quinta ejecuta el filtrado: el functor será llamado para cada elemento y sólo los que cumplan la condición se almacenarán en la lista results. Por último, invocamos el functor de impresión que creamos anteriormente y pasamos los iteradores de results.

Y así, Charlie Brown, es como podemos usar functores para realizar filtros sobre colecciones. La verdad es que for_each nos ayuda bastante, aunque es cierto que crear una clase functor puede resultar tedioso. En los ejemplos anteriores, los functores son clases un poco más complicadas ya que permiten realizar ciertas configuraciones (al menos con filter_employee). Habrá casos, por supuesto, en los que no valga la pena crear un functor. Por ejemplo, print_employee sale sobrando. Para estas situaciones podemos usar lambdas.

Los lambdas son funciones anónimas, introducidas en C++ a partir del estándar C++11 (es decir, el del 2011). Aunque son recientes, afortunadamente Visual C++ 2010 ya las implementa, por lo que podemos hacer uso de ellas (obvio, si usas VC++; aunque supongo que otros compiladores de C++ ya habrán implementado los lambdas).

El siguiente código muestra un lambda para imprimir el empleado, así como su uso junto con for_each.

auto print_func = [](const employee& value) { wcout << value._name << endl; };

for_each(employees.begin(), employees.end(), print_func);

Como puedes ver, print_func es una función lambda. Los corchetes [] así lo indican, seguido de los parámetros (en este caso, una referencia constante a un employee), seguido del cuerpo de la función: en este caso, la impresión hacia la consola del nombre. Por supuesto, los lambdas pueden ser más complejos: podemos crear una también para filtrar por nombre, como en el siguiente código.

auto filter_func = [&results](const employee& value) { 
    if (value._name.find(L"P") != wstring::npos)
        results.push_back(value);
};
auto print_func = [](const employee& value) { wcout << value._name << endl; };
for_each(employees.begin(), employees.end(), filter_func);
for_each(results.begin(), results.end(), print_func);

En la primera línea del código anterior declaramos nuestro lambda. Nota que en esta ocasión entre los corchetes hemos colocado “&results”. Recordamos que results es la lista donde almacenamos los resultados. Así, esa expresión significa: pasa una referencia hacia results como parámetro a la función lambda. Si no ponemos nada entre corchetes, no podemos hacer referencia hacia results. Si dejamos el ámperson nada más (i.e. [&](const employee& value)) entonces indicamos que todas las variables que se declaren fuera de la lambda sean asequibles por referencia. Si colocamos el nombre results sin el ámperson, entonces quiere decir que lo pasamos por valor.

Así, como puedes ver, podemos usar cualquiera de los dos enfoques para realizar filtrado sobre un contenedor. Por supuesto que hay otros, pero estos son los que mejor me han funcionado. Mi recomendación es que juegues un poco con el código anterior y practiques.

¡Suerte!

Categorías:Apunte, C++ Etiquetas: , ,
  1. Aún no hay comentarios.
  1. febrero 2, 2012 a las 5:24 pm

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