Artículos de la serie:
- Introducción.
- Propiedad exclusiva (primera parte).
- Propiedad exclusiva (segunda parte).
- Propiedad compartida (primer parte).
- Propiedad compartida (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:
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.
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.
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 = nullptr) noexcept -> 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