Punteros inteligentes (I): Introducción

Última actualización de esta serie: 1 de agosto de 2020

Artículos de la serie:
  1. Introducción.
  2. Propiedad exclusiva (primera parte).
  3. Propiedad exclusiva (segunda parte).
  4. Propiedad compartida (primer parte).
  5. Propiedad compartida (segunda parte).

Introducción


Una de las principales lecciones aprendidas tras décadas de uso de C++ es la de que los punteros tradicionales no-encapsulados no deberían emplearse como agentes propietarios de los objetos a los que apuntan. 

En efecto, consideremos el uso no-encapsulado de una expresión new para alojar un objeto en el free store. Ello exige al programador el uso posterior de delete para destruir el objeto y liberar su memoria, así como la introducción de engorrosos bloques de limpieza try-cacth para evitar fugas de memoria ante la posible emisión de excepciones. Por ejemplo:

   S* p = new S{data};    try {       auto& obj = *p;       // usamos el objeto apuntado por p, lo que puede provocar excepciones...    }    catch (...) { // en caso de producirse una excepción       delete p;  // destruimos el objeto referenciado y liberamos la memoria       throw;     // relanzamos la excepción    }    delete p;     // en caso de no haberse producido excepciones     

Recordemos, asimismo, que llamar a delete más de una vez sobre el mismo puntero conduce a comportamiento indefinido (undefined behavior). Detalles como éste complican la gestión de la memoria a través del uso explícito de new y delete.

Los estándares C++11 y posteriores proporcionan una familia de punteros inteligentes para lidiar adecuadamente con este tipo de problemas. Un puntero inteligente es una abstracción, típicamente implementada como una plantilla de clase (class template), que imita el comportamiento de un puntero tradicional mediante la sobrecarga de operadores. Sin embargo, en contraste con los punteros tradicionales, los punteros inteligentes pueden gestionar de forma automática el correcto cierre/destrucción de los recursos que referencian, garantizando la seguridad ante excepciones. Regresando a nuestro ejemplo anterior, las operaciones de adquisición y liberación de memoria podrían quedar encapsuladas mediante el empleo de la técnica RAII (ya analizada en una serie de posts anterior), siguiendo un esquema básico similar al siguiente:

   template<typename T>    class Smart_pointer {        T* p_;    public:       explicit Smart_pointer(T* p) : p_{p} { } // el constructor se hace cargo del recurso       ~Smart_pointer() { delete p_; } // el destructor libera el recurso       // otras funciones de acceso al recurso apuntado...    };

Como bien sabemos, el destructor de esta clase será implícitamente invocado cuando el puntero inteligente abandone el ámbito en que fue creado o cuando se produzca una excepción. Dicho destructor se encargará entonces de llamar a delete con el fin de destruir el objeto referenciado y liberar la memoria asignada en su construcción. Ello simplifica sustancialmente el primer código de ejemplo de esta introducción:

   {       auto p = Smart_pointer<S>{new S{data}};       // usamos el objeto apuntado por p...    } // delete es invocado automáticamente en este punto

En los próximos posts analizaremos las clases de punteros inteligentes proporcionadas por la biblioteca estándar del lenguaje en el fichero de cabecera <memory>. En función de la semántica de propiedad ofrecida, distinguiremos:
  • std::unique_ptr: propiedad exclusiva.
  • std::shared_ptr: propiedad compartida.
  • std::weak_ptr: referencia débil (no-propietaria) a un objeto gestionado por std::shared_ptr.


Siguiente artículo de la serie:

No hay comentarios:

Publicar un comentario