Punteros inteligentes (II): Propiedad exclusiva (primera parte)

Puntero inteligente std::unique_ptr


La plantilla de clase std::unique_ptr<> implementa una semántica de propiedad exclusiva del objeto apuntado. Concretamente, un objeto u de tipo std::unique_ptr<T> almacena un puntero miembro a un segundo objeto de tipo T o derivado, sobre el que se sigue una determinada política de destrucción (configurable por el usuario) cuando u es destruido. La semántica de propiedad exclusiva implica que el puntero inteligente u no pueda copiarse, pues de poder hacerlo acabaríamos con dos punteros propietarios del mismo objeto. Existe, sin embargo, un mecanismo de transferencia de la propiedad que analizaremos en este post.

El estándar del lenguaje define la siguiente plantilla biparamétrica para tipos T no-matriciales (analizaremos los tipos matriciales en el tercer artículo de esta serie):

   namespace std {       template<typename T, class D = default_delete<T>> class unique_ptr;    }

El segundo parámetro D determina la política de liberación a seguir con el objeto apuntado al finalizar el tiempo de vida del puntero inteligente. D debe ser un objeto función, una referencia lvalue a un objeto función o una referencia lvalue a una función, invocable con un argumento de tipo std::unique_ptr<T,D>::pointer. Como veremos en el próximo artículo de esta serie, std::unique_ptr no está concebido únicamente para la gestión de objetos en el free store. Sin embargo, tal es el uso habitual de este puntero, por lo que la política de destrucción adoptada por defecto std::default_delete<T> empleará delete para destruir el objeto apuntado y desalojar la memoria por él ocupada:

   namespace std {       template<typename T>       struct default_delete {          default_delete() noexcept = default;          template<typename U>          default_delete(default_delete<U> const&noexcept { }          void operator()(T* p) const          {             // si T es un tipo incompleto, el programa no está bien definido             static_assert(sizeof(T) > 0, "cannot delete pointer to incomplete type");             delete p;          }       };    }

Con el fin de obtener un código seguro y robusto en C++ moderno, se recomienda evitar el uso no encapsulado de expresiones new/delete. Es por ello que los estándares C++14 y posteriores proporcionan la plantilla de función variádica std::make_unique con el fin de inicializar punteros std::unique_ptr sin emplear new explícitamente:

   namespace std {       template<typename T, typename ...Args>       std::enable_if_t<!std::is_array<T>::value, std::unique_ptr<T>>       make_unique(Args&&... args)       {          return std::unique_ptr<T>(new T(std::forward<Args>(args)...));       }    }

Esta función ubica un nuevo objeto de tipo T en el free store a través de una expresión new, remitiendo los argumentos args al constructor de T mediante referencias de reenvío (perfect forwarding). El puntero retornado por new es entonces empleado para inicializar un objeto std::unique_ptr<T>, cuya propiedad es transferida finalmente al programador.


Ejemplo de uso


De ser necesario el uso explícito de punteros, std::unique_ptr se considera la opción por defecto para la gestión segura ante excepciones de objetos con duración de almacenamiento dinámica. Con el fin de comprender su modo de funcionamiento, consideremos el código siguiente:

   struct Book {       std::string title;       double price;    };        {       auto u = std::make_unique<Book>("War and Peace"22.95); // (1) válido en C++20       Book& book = *u;               // (2)       book.price = 19.95;            // (3)       auto v = std::move(u);         // (4)       assert(!u);           // (5) ok, continuamos       std::cout << v->price;         // (6) output: 19.95    } // (7)

En (1) se inicializa una variable local de tipo std::unique_ptr<Book>, que apunta a un objeto de tipo Book almacenado en el free store. Hagamos notar que C++20 permite el uso directo de std::make_unique con agregados de datos.

En (2), la sobrecarga del operador de indirección (*) del puntero inteligente permite acceder al objeto apuntado por u y definir un alias (book) para el mismo. Así, por ejemplo, la línea (3) modifica el precio del libro, sobrescribiendo su valor original de 22.95 a 19.95.

Con el fin de dar efecto a la semántica de propiedad exclusiva, std::unique_ptr posee un constructor copia y un operador de asignación copia declarados delete, por lo que tales operaciones se encuentran deshabilitadas:

   namespace std {        template<typename T, class D = default_delete<T>>       class unique_ptr {       public:          // unique_ptr no es CopyConstructible ni CopyAssignable:          unique_ptr(unique_ptr const&) = delete;          unique_ptr& operator=(unique_ptr const&) = delete;          // unique_ptr es MoveConstructible y MoveAssignable:          unique_ptr(unique_ptr&&noexcept;          unique_ptr& operator=(unique_ptr&&noexcept;          // ...       };    }

De ahí que se produjese un error de compilación de intentar realizar una inicialización como la siguiente:

   auto v = u; // error de compilación

Sin embargo, std::unique_ptr posee semántica de movimiento, de modo que la propiedad del objeto referenciado por u puede ser transferida a un nuevo puntero inteligente v de tipo std::unique_ptr<Book> de manera explícita como en (4), sin más que utilizar la función std::move definida en el fichero de cabecera <utility>. Al hacerlo, el puntero miembro de u es reseteado a nullptr, tal y como lo atestigua la aserción (5). A partir de (4), el puntero v es, por tanto, el responsable único del objeto Book creado en la línea (1). El puntero u, si bien vacío, sigue disponible para una eventual reasignación (C++, como vemos, opta por operaciones de movimiento no destructivas). Eso sí, cualquier desreferencia del puntero u en dicho estado daría lugar a comportamiento indefinido (undefined behavior).


La sobrecarga del operador de desreferencia (->) permite acceder y mostrar en consola el precio actual del libro (6) a través del puntero v.

En (7), tanto u como v salen fuera de ámbito, invocándose delete automáticamente sobre sus punteros miembro. Esta operación no tiene ningún efecto en u, al ser su puntero miembro nulo. En el caso de v, sin embargo, el objeto por él apuntado es destruido y su memoria liberada.


Siguiente artículo de la serie:

No hay comentarios:

Publicar un comentario