Artículos de la serie:
- Introducción.
- Propiedad exclusiva (primera parte).
- Propiedad exclusiva (segunda parte).
- Propiedad compartida (primer parte).
- Propiedad compartida (segunda 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:
- Sutter's Mill - https://herbsutter.com/2011/10/25/garbage-collection-synopsis-and-c/
- std::shared_ptr - https://en.cppreference.com/w/cpp/memory/shared_ptr
- std::make_shared - https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared
- std::allocate_shared - https://en.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared
- Scott Meyers, Effective Modern C++. O'Reilly Media (2014).
- B. Stroustrup C++11 FAQ - http://stroustrup.com/C++11FAQ.html#std-shared_ptr
No hay comentarios:
Publicar un comentario