Punteros inteligentes (III): Propiedad exclusiva (segunda parte)

Políticas de liberación


Ha llegado el momento de analizar cómo configurar la política de liberación a seguir con el objeto apuntado por std::unique_ptr al finalizar el tiempo de vida del puntero inteligente. A modo de ejemplo, empleemos std::unique_ptr para garantizar que un flujo a fichero std::FILE sea cerrado automáticamente al término de un ámbito:

   using File_ptr = std::unique_ptr<std::FILE,                                     decltype([](std::FILE* f){ if (f) std::fclose(f); })>;    {       auto ifile = File_ptr{std::fopen("text.txt", "r")};                 // usamos el fichero en modo lectura...              } // el destructor de File_ptr cierra el fichero automáticamente en este punto

Hemos hecho uso aquí del estándar C++20, que permite la aparición de expresiones lambda en contextos no-evaluados (unevaluated contexts) como decltype. En nuestro código, el objeto std::unique_ptr se hace cargo del puntero std::FILE* retornado por la función std::fopen e inicializa un deleter a partir de la expresión lambda. Dicho deleter es el encargado de cerrar el fichero una vez que el puntero inteligente finalice su tiempo de vida. Observemos, en particular, que en ningún punto se ha efectuado alojamiento dinámico de memoria.


Polimorfismo dinámico


Al igual que los punteros tradicionales, std::unique_ptr<Derived> será implícitamente convertible a std::unique_ptr<Base> siempre que Derived* sea implícitamente convertible a Base*:

   struct Base {       virtual void message() const = 0;       virtual ~Base() = default;    };    struct Derived : Base {       void message() const       override {          std::cout << "Derived::message()";       }    };    auto p = std::unique_ptr<Base>{std::make_unique<Derived>()};    p->message(); // output: "Derived::message()"

Aun cuando el tipo estático del puntero p en el código anterior sea std::unique_ptr<Base>, éste apunta a un objeto Derived ubicado en el free store. Como era de esperar, la llamada a la función virtual message() se resuelve en base a este último tipo dinámico.

Por ejemplo, consideremos la siguiente jerarquía de clases:

struct Dinosaur { // interfaz     virtual auto species() const -> std::string = 0;     virtual ~Dinosaur() = default; }; struct Brachio : Dinosaur {     auto species() const -> std::string override { return "brachiosaurus"; } }; struct Raptor : Dinosaur {     auto species() const -> std::string override { return "velociraptor"; } }; struct Trex : Dinosaur {     auto species() const -> std::string override { return "tyrannosaurus rex"; } };

Gracias a std::unique_ptr podemos crear un vector de punteros a la clase base Dinosaur que apunten a objetos de las clases derivadas. Al destruirse el contenedor, se destruirán los punteros inteligentes, lo que conllevará la liberación automática de los recursos en memoria:

   {       using Dino_ptr = std::unique_ptr<Dinosaur>;     auto dinos = std::vector<Dino_ptr>{};       dinos.emplace_back(std::make_unique<Raptor>());       dinos.emplace_back(std::make_unique<Brachio>());       dinos.emplace_back(std::make_unique<Trex>());       for (auto const& p : dinos) {          if (dynamic_cast<Trex*>(p.get()))             std::cout << "The biggest carnivore on Earth!: ";          std::cout << p->species() << '\n';       }    } // limpieza automática de la memoria en este punto

Observemos que, con el fin de imprimir un mensaje especial para objetos de tipo Trex, invocamos la función get() del puntero inteligente para obtener un puntero tradicional al objeto referenciado; dicha dirección es entonces sometida a una conversión dynamic_cast a Trex* para comprobar si el objeto es del tipo esperado.

Señalemos, por último, que std::unique_ptr debiera ser el tipo de puntero retornado por defecto por las factorías, pudiendo convertirse posteriormente en punteros std::shared_ptr (propiedad compartida) de ser necesario:

auto create_dino(int level) -> std::unique_ptr<Dinosaur> {     assert(level>=0 and level<=2); // precondición     auto dino = std::unique_ptr<Dinosaur>{};     switch (level) {        case 0: dino = std::make_unique<Brachio>(); break;        case 1: dino = std::make_unique<Raptor>(); break;        case 2: dino = std::make_unique<Trex>(); break;        default: std::abort();     }     return dino; }

Comparemos el código anterior con una implementación característica de C++98/03, que hubiese requerido el retorno de un puntero tradicional Dinosaur* por parte de la factoría create_dino, requiriendo así la destrucción manual mediante delete de los objetos obtenidos. En contraste, nuestra función explicita de forma clara la semántica de propiedad proporcionada por la factoría y permite la gestión automática de los tiempo de vida.


Tipos matriciales


El lenguaje proporciona también las especializaciones para tipos matriciales de tamaño conocido en tiempo de ejecución std::unique_ptr<T[]> y std::make_unique<T[]>. A modo de ejemplo:

   auto sz = std::size_t{};    std::cin >> sz;    auto p = std::make_unique<int[]>(sz); // array de longitud sz alojado en el free store    for (auto i = std::size_t{}; i < sz; ++i)  // llenamos el array       std::cin >> p[i];    std::sort(p.get(), p.get() + sz, std::greater{}); // ordenamos el array de mayor a menor    for (auto i = std::size_t{}; i < sz; ++i) // mostramos los datos en consola       std::cout << p[i];


Implementación de la semántica de propiedad exclusiva


A efectos puramente ilustrativos, el siguiente código facilita una implementación de un puntero inteligente Unique_ptr con semántica de propiedad exclusiva para objetos no-matriciales. Con el fin de simplificar su análisis, la única política de destrucción contemplada es la de liberación de memoria mediante delete:

   template<typename T>    class Unique_ptr {       static_assert(!std::is_array_v<T>, "Value type cannot be of array type");       T* p_; // puntero almacenado    public:       // construcción y destrucción:       Unique_ptr() noexcept : p_{nullptr} { }       explicit Unique_ptr(T* p) noexcept : p_{p} { }       ~Unique_ptr() { delete p_; }       // no se puede copiar desde un (propiedad exclusiva)       // Unique_ptr no es CopyConstructible ni CopyAssignable       Unique_ptr(Unique_ptr const&) = delete;       Unique_ptr& operator=(Unique_ptr const&) = delete;       // permitimos transferir la propiedad a otro Unique_ptr       // Unique_ptr es MoveConstructible y MoveAssignable       Unique_ptr(Unique_ptr&& other) noexcept : p_{other.p_} { other.p_ = nullptr; }       auto& operator=(Unique_ptr&& rhs) noexcept       {          reset(rhs.release());          return *this;       }       template<typename U> // U* implícitamente convertible a T*       Unique_ptr(Unique_ptr<U>&& other) noexcept : p_{other.p_} { other.p_ = nullptr; }       template<typename U> // U* implícitamente convertible a T*       auto& operator=(Unique_ptr<U>&& rhs) noexcept       {          reset(rhs.release());          return *this;       }       // observadores:       auto  operator->() const noexcept { return p_; }       auto& operator*()  const noexcept { return *p_; }       auto  get()   const noexcept { return p_; }       explicit operator bool() const noexcept { return (p_ != nullptr); }       // modificadores:       auto reset(T* p = nullptrnoexcept -> void       {          std::swap(p_, p);          delete p;       }       auto release() noexcept -> T* // renunciamos a la propiedad       {          auto p = p_;          p_ = nullptr;          return p;       }    };


Siguiente artículo de la serie:

No hay comentarios:

Publicar un comentario