Expresiones lambda (II)

Artículos de la serie:

Objetos función con estado interno


Los objetos función extienden la utilidad de las funciones tradicionales gracias a la posibilidad de poseer estado interno y proporcionar operaciones adicionales.

A modo de ejemplo, consideremos el problema sencillo de calcular la media aritmética de un conjunto finito de valores numéricos almacenados en coma flotante X = {x1, x2, ... , xn}:

 X = (x1 + x2 + ... + xn)/n.

Procedamos a implementar una plantilla de clase Mean<> que, tras la llamada i-ésima a su función miembro pública operator(), encapsule la suma de los primeros i-ésimos valores del conjunto X, así como un registro del número i de elementos acumulados hasta entonces:

   template<std::floating_point T>    class Mean {       T sum_{};       std::size_t ith_{};    public:       Mean() = default;       auto operator()(T const& val) -> void { sum_ += val; ++ith_; }       auto get() const -> T { return sum_/ith_; }    };

Observemos que todas las funciones miembro de la interfaz pública anterior pueden ser declaradas constexpr y noexcept, si bien hemos preferido no hacerlo explícito en el código con el fin de facilitar su lectura.

La función miembro pública get() permite obtener la media de los valores acumulados tras la última ejecución del operador operator(). El modo de uso de esta plantilla para obtener la media X sería, pues, el siguiente:

   auto const v = std::array{1.0, 2.0, 3.0, 4.0};    auto m = Mean<double>{};    for (auto d : v)       m(d);    std::cout << m.get(); // output: 2.5

El uso combinado de algoritmos de la biblioteca estándar y objetos función con estado presenta algunas sutilezas contra las cuales el programador debe estar prevenido. En primer lugar, el orden de iteración a lo largo de un rango [first,last) no se encuentra especificado en la mayoría de estos algoritmos. No puede confiarse, pues, en que la llamada al objeto función se produzca de manera secuencial a lo largo del rango.

En segundo lugar, debemos tener en cuenta que los algoritmos proporcionados por la biblioteca estándar están diseñados para tomar por valor los objetos función pasados como argumentos. El propio estándar ISO C++ especifica que los algoritmos son libres de copiar tales objetos función tantas veces como sea necesario durante su ejecución.

Este hecho resulta particularmente crítico al trabajar con predicados, siendo ésta la razón por la que tradicionalmente se recomiende que dichos objetos función sean puros, es decir, operator() debe devolver un booleano determinado exclusivamente por los valores pasados como argumento y no puede tener efectos secundarios. Ello imposibilita la modificación de datos miembros del objeto función durante la llamada al operador [1]. Asimismo, el que los objetos función sean pasados por valor conduce a efectos de slicing en tipos polimórficos, desaconsejándose por ello su uso.

No debiera constituir ninguna sorpresa, pues, que el siguiente código retorne NAN como resultado del cálculo de la media:

   auto const v = std::array{1.0, 2.0, 3.0, 4.0};    auto m = Mean<double>{}; // (A)    std::for_each(v.begin(), v.end(), m); // (B)    std::cout << m.get(); // output: nan

En efecto, el algoritmo std::for_each() proporcionado en el fichero de cabecera estándar <algorithm> posee como signatura:

  namespace std {     template<typename InputIt, typename UnaryFunction>     constexpr auto for_each(InputIt first, InputIt last, UnaryFunction f) -> UnaryFunction;   }

Observemos, en particular, cómo el objeto función unario f se toma por valor. En consecuencia, el objeto m creado en (A) antes de la llamada a std::for_each() no llega a ser modificado por el algoritmo, lo que da pie a una indeterminación del tipo 0/0 como media del conjunto. El cálculo deseado de la media se produce, en realidad, sobre la copia temporal de m creada en (B) al pasar dicho objeto por valor como argumento a la función. Dicho temporal es destruido automáticamente al finalizar la ejecución del algoritmo. En aquellos casos como el aquí considerado en que la identidad del objeto función sea importante, el usuario debe considerar el uso de una clase wrapper copiable cuyas instancias referencien permanentemente a un mismo objeto función. Para ello, podemos hacer uso de objetos de la plantilla de clase estándar std::reference_wrapper<>, generados por las funciones std::cref() o std::ref() según se requiera una referencia constante o no constante, respectivamente [2]: 

   auto m = Mean<double>{};    std::for_each(v.begin(), v.end(), std::ref(m));    std::cout << m.get(); // output: 2.5


Referencias bibliográficas:
  1. Meyers S., Effective STL, Addison-Wesley (2001). Véase Item 39.
  2. Cppreference - std::ref std::cref - https://en.cppreference.com/w/cpp/utility/functional/ref

No hay comentarios:

Publicar un comentario