Punteros inteligentes (IV): Propiedad compartida (primera parte)


Propiedad compartida. Bloques de control


La plantilla de clase uniparamétrica std::shared_ptr<> implementa una semántica de propiedad compartida: uno o más objetos std::shared_ptr<> pueden ser propietarios no-exclusivos de un mismo recurso (normalmente, memoria asignada dinámicamente mediante expresiones new). Dicho recurso es liberado una vez que todos los objetos std::shared_ptr<> que lo referencien sean destruidos, reasignados o renuncien a su propiedad, siendo su último propietario el encargado de liberarlo (la política de liberación por defecto es una expresión delete, aunque ésta puede ser personalizada por el usuario). Este comportamiento se consigue a través de un contador de referencias interno, lo que convierte a std::shared_ptr<> en un mecanismo básico de recolección de basura cuando el recurso compartido sea memoria [1].

Sea T un tipo no matricial. Un puntero std::shared_ptr<T> suele consistir en dos punteros tradicionales internos [2]. Uno de ellos, retornado por la función miembro pública get(), es el puntero almacenado de tipo T*. El otro es un puntero a un bloque de control que contiene:
  • Un puntero al objeto apuntado (de tipo T o derivado), o bien el propio objeto en sí.
  • Un contador atómico con el número de objetos std::shared_ptr que comparten la propiedad del objeto. Dicho número es accesible a través de la función miembro use_count().
  • Un contador atómico con el número de punteros std::weak_ptr que apuntan al objeto.
  • La política de liberación (deleter) y el alojador (allocator) posiblemente personalizados por el usuario, ambos sometidos a type-erasure.
De emplear los constructores propios de la plantilla std::shared_ptr<>, el objeto apuntado y el bloque de control serán alojados de forma independiente. En tal caso, el bloque de control contendrá un puntero al objeto. Por contra, de obtener el puntero inteligente a través de una llamada a las funciones estándar std::make_shared() [3] o std::allocate_shared() [4], el objeto será un dato miembro más del bloque de control (ver imagen derecha [5]). La memoria necesaria para alojar al bloque es reservada en este último caso en una única operación. La función std::allocate_shared() se emplea, en particular, cuando queremos establecer un alojador personalizado.

El puntero u objeto almacenado en el bloque de control es aquél que se libera cuando el número de punteros std::shared_ptr propietarios decae a cero. El bloque de control en sí no será desalojado hasta que el contador de punteros std::weak_ptr alcance también cero.

El siguiente código de ejemplo hace uso de la función estándar std::make_shared() [2] para alojar un entero en el free store y compartir su propiedad con otro puntero inteligente:

   {       auto p1 = std::make_shared<int>(0);       {          auto p2 = p1; // p2 comparte la propiedad del entero con p1                        // y el contador de referencias se incrementa a 2          *p2 = 1; // el entero vale ahora 1       } // p2 es destruido, el contador decae a 1       std::cout << *p1; // imprime 1    } // p1 es destruido, el contador decae a 0 y la memoria es liberada

Para personalizar la política de liberación del recurso, basta con pasar un objeto de la clase que la implemente como argumento al contructor de std::shared_ptr<>. Por ejemplo:

   {       using namespace std;              auto cls = [](FILE* f){ if (f) { cout << "Close the file"; fclose(f); } };       auto p1 = shared_ptr<FILE>{fopen("text.txt", "w"), cls};       cout << "Number of shared_ptrs referring to the same file:\n"          << "After creating p1: " << p1.use_count() << '\n';              auto p2 = p1;       cout << "After copy-initializing p2: " << p1.use_count() << '\n';              p2.reset(); // renuncia a la propiedad compartida del fichero       cout << "After reseting p2 to nullptr: " << p1.use_count()          << "\nExit main() and destroy p1" << '\n';    } // se destruye p1, el contador de referencias decae a 0 y se cierra el fichero    /* Output:       Number of shared_ptrs referring to the same file:       After creating p1: 1       After copy-initializing p2: 2       After reseting p2 to nullptr: 1       Exit main() and destroy p1       Close the file    */

Encontramos un ejemplo típico de la utilidad del concepto de propiedad compartida en la implementación de grafos acíclicos dirigidos [6], donde cada nodo mantiene una lista de punteros a sus nodos hijos (véase la figura inferior): 


   struct Node {       std::vector<std::shared_ptr<Node>> children;       std::vector<Node*> parents;       // ...    };

La lista de nodos hijos almacena punteros std::share_ptr<> con el fin de garantizar que aquéllos permanezcan en memoria mientras posean al menos un nodo padre. De requerirse también una lista de punteros a los nodos padres, basta que éstos sean punteros tradicionales no-propietarios.


Referencias bibliográficas:
  1. Sutter's Mill - https://herbsutter.com/2011/10/25/garbage-collection-synopsis-and-c/
  2. std::shared_ptr - https://en.cppreference.com/w/cpp/memory/shared_ptr
  3. std::make_shared - https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared
  4. std::allocate_shared - https://en.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared
  5. Scott Meyers, Effective Modern C++. O'Reilly Media (2014).
  6. B. Stroustrup C++11 FAQ - http://stroustrup.com/C++11FAQ.html#std-shared_ptr
Siguiente artículo de la serie:

No hay comentarios:

Publicar un comentario