Punteros inteligentes (V): Propiedad compartida (segunda parte)


std::weak_ptr


La plantilla de clase std::weak_ptr<>, el tercer tipo de puntero inteligente que analizaremos en esta serie de posts, se encuentra intrínsecamente ligada al uso de la propiedad compartida, permitiendo interrumpir referencias circulares de objetos std::shared_ptr<>.

A modo de ejemplo, consideremos el patrón de diseño de nombre observador (observer pattern en Inglés), propio de la programación orientada a objetos. Se desea implementar un notificador al que pueda suscribirse un número arbitrario de observadores. El notificador deberá informar de forma automática a todos los observadores registrados de cualquier cambio en su estado mediante el envío de un mensaje, sin que esto afecte sustancialmente a la implementación de sus clases.

Se recomienda al lector interesado en profundizar en éste y otros patrones de diseño que consulte la obra clásica "Design Patterns", de E. Gamma et al., referencia obligada en este campo.

De forma especialmente señalada, observemos que al realizar una notificación, el notificador deberá comprobar si los observadores suscritos existen aún. En efecto, el hecho de pertenecer a una lista de notificación no implica que un observador no haya sido destruido durante el tiempo de vida del notificador. En el peor de los casos, un usuario puede haber destruido un observador, olvidando borrarlo de la lista de suscripción del notificador. En tal caso, debemos evitar enviar un mensaje a un observador inexistente. Una discusión similar nos lleva a concluir que, de existir el observador, éste no debe poder ser destruido mientras se realice la notificación.

Queda claro, pues, que una implementación de la lista de observadores basada en punteros std::shared_ptr<> es claramente inapropiada, pues el notificador no debe compartir la propiedad de los observadores salvo en el momento de informar de su cambio de estado. Es precisamente con el fin de lograr dicho comportamiento que el estándar del lenguaje proporciona los punteros std::weak_ptr<>. Con esta discusión en mente, empecemos definiendo un nuevo tipo Observer_set (la lista de suscriptores del notificador) como un conjunto de punteros std::weak_ptr<> a los distintos observadores suscritos:

   using Observer_key = std::weak_ptr<Observer>;    using Observer_set = std::set<Observer_key, std::owner_less<Observer_key>>;
En este contexto, sería más eficiente el uso de un conjunto no-ordenado std::unordered_set<>, pero desafortunadamente el lenguaje C++ no dispone aún de una función hash estándar para punteros std::weak_ptr<>.
Aquí, Observer es la clase base abstracta de la que deben derivar todas las clases de observadores:

   struct Message { /* ... */ }; // tipo de mensaje enviado por el notificador    struct Observer {       virtual void update(Message const&) = 0;       virtual ~Observer() = default;    };

Todas las clases derivadas deben, pues, sobrescribir la función update() indicando el tipo de acción a emprender al recibir un mensaje (un objeto de la clase Message) por parte del notificador.

La inserción y eliminación de observadores en el registro (un objeto de la clase Observer_set) es inmediata de implementar sin más que consultar la interfaz pública del contenedor estándar std::set<> (observemos que utilizamos objetos std::shared_ptr<> para inicializar los punteros std::weak_ptr<>):

   auto attach(Observer_set& os, std::shared_ptr<Observer> const& p) -> bool    {       auto const key = Observer_key{p};       // retornamos un booleano indicando si la inserción tuvo lugar:       return os.insert(key).second;    }    auto detach(Observer_set& os, std::shared_ptr<Observer> const& p) -> bool    {       return os.erase(Observer_key{p});    }

Dado un cambio de estado del notificador, y con el fin de enviar una notificación a los observadores registrados, definiremos una función de nombre notify_all(). Ésta invoca a la función miembro pública lock() sobre cada uno de los punteros std::weak_ptr<> almacenados en la lista de suscriptores. De existir el observador, esta función retornará un puntero std::shared_ptr<> no-vacío que lo referencie, compartiendo su propiedad (y evitando su destrucción) mientras se produzca la notificación. Si el observador ha dejado de existir, el puntero std::shared_ptr<> devuelto por la función estará vacío, en cuyo caso puede procederse a la eliminación inmediata del observador en la lista de suscriptores:

   void notify_all(Observer_set& os, Message const& mssg)    {       for (auto it = os.begin(); it != os.end(); /* no-op */) {          if (auto observer = it->lock()) { // el observador existe             observer->update(mssg); // notificamos al observador             ++it;          }          else             it = os.erase(it); // eliminamos el puntero weak_ptr del conjunto de observadores       }    }

Por supuesto, el diseño analizado en este post sería más complicado si deseáramos garantizar la integridad de la lista de notificación como recurso compartido por varios hilos de ejecución diferentes, un punto éste que podrá ser discutido en el futuro cuando analicemos las técnicas de programación concurrente en C++.

No hay comentarios:

Publicar un comentario