Expresiones lambda (I)

Última actualización: 30 de agosto de 2020.

Artículos de la serie:

Objectos función (function objects)


Antes de poder entender qué son y cómo funcionan las expresiones lambda, es preciso que comprendamos el concepto de objeto función.

En el lenguaje C++ las clases pueden sobrecargar el operador llamada a función operator() con el fin de dotar a sus objetos del comportamiento propio de las funciones tradicionales. A modo de ejemplo, consideremos la plantilla estándar std::greater<> proporcionada en el fichero de cabecera <functional>, destinada a realizar la comparación de dos valores dados a través del operador "mayor estricto que" operator>. Una posible implementación de esta plantilla en C++20 vendría dada por:

namespace std {      template<typename T>     struct greater {        constexpr auto operator()(T const& x, T const& y) const -> boolreturn x > y; }     }; }

Dado un objeto de la clase anterior auto g = std::greater<int>{} y dos valores cualesquiera a y b convertibles a tipo int, la expresión g(a,b) será interpretada como una llamada explícita al operator operator(), resolviéndose como g.operator()(a,b).

Obviamente, el booleano retornado será verdadero o falso en función del orden de los argumentos:

   using namespace std;    auto g = greater<int>{};    cout << boolalpha << g(3,4) << '\n' // equivalente a g.operator()(3,4); output: false                      << g(4,3);        // equivalente a g.operator()(4,3); output: true

Con carácter general, el estándar del lenguaje define como tipo de objeto función (function object type) a todo aquél que pueda ser utilizado a la izquierda del operador llamada a función

postfix-expression(expression-listopt).

Entre estos tipos, cabe distinguir:
  • Punteros a funciones.
  • Clases con sobrecargas del operador operator() como función miembro pública. Estas clases son tradicionalmente conocidas con el sobrenombre inglés de functors.
  • Clases convertibles a punteros a función.
Un objeto función (function object) es un objeto cualquiera de uno de los tipos anteriores.

Observemos que una función tradicional no constituye un objeto función, pero puede emplearse allá donde se espere un objeto función gracias a su conversión implícita en puntero a función.

Los algoritmos de la biblioteca estándar que requieran el uso de funciones externas están diseñados para aceptar objetos función por valor, y no únicamente punteros a función como sería natural en un código en C. Tal es el caso del algoritmo de ordenación std::sort():

template<typename RandomIt, typename Compare>  constexpr auto sort(RandomIt first, RandomIt last, Compare comp) -> void;

Aquí, comp es un objeto función (en este caso, un predicado binario) que establece la política de ordenación (orden débil estricto) a seguir. Por ejemplo, este algoritmo, en conjunción con la estructura std::greater<> descrita anteriormente, permite ordenar un vector de enteros por orden estrictamente decreciente de elementos. En el siguiente código, un objeto temporal std::greater<int> es inicializado directamente como tercer argumento de la función std::sort:

   auto v = std::vector<int>{1, 7, -2, 0, 5}; std::sort(v.begin(), v.end(), std::greater<int>{}); // v es ordenado a {7,5,1,0,-2}

De forma conveniente, podemos servirnos de la funcionalidad CTAD de C++17 (Class Template Argument Deduction) para deducir de forma automática tanto el tipo de elemento contenido en el vector como el tipo de elemento a ordenar:

   auto v = std::vector{1, 7, -2, 0, 5}; // v es de tipo std::vector<int> std::sort(v.begin(), v.end(), std::greater{}); // ordenación de enteros

El lector quizás se pregunte las ventajas que puede obtener un programador al trabajar con clases functors frente a las funciones tradicionales. Si bien comentaremos importantes funcionalidades en próximos posts, citaremos aquí un aspecto esencial relacionado con la eficiencia del código: Al trabajar con functors, los compiladores han sido tradicionalmente capaces de reemplazar con facilidad las llamadas a la función miembro operator() por el cuerpo del operador en el código fuente. Ello incrementa notablemente la eficiencia de algoritmos como std::sort() al evitar el coste de repetidas llamadas al operador. Esto contrasta vivamente con el uso de punteros a función, cuya sustitución en línea (inline) depende críticamente de las optimizaciones realizables por el compilador, así como de la estructura/complejidad de la función en cuestión (véase el tutorial proporcionado por MSDN Channel 9, citado en las referencias bibliográficas, para más detalles).


Referencias bibliográficas:
  • Stroustrup B., 'The C++ Programming Language'. Addison-Wesley, 4th Edition (2013).
  • https://eel.is/c++draft/function.objects
  • MSDN Channel 9 - Stephan T. Lavavej - https://channel9.msdn.com/Series/C9-Lectures-Stephan-T-Lavavej-Standard-Template-Library-STL-/C9-Lectures-Stephan-T-Lavavej-Standard-Template-Library-STL-6-of-n

No hay comentarios:

Publicar un comentario