Ir al contenido

std::unique_ptr: Propiedad Exclusiva

Aprende como std::unique_ptr proporciona propiedad exclusiva sin sobrecarga, como usar make_unique, deleters personalizados para recursos C, y patrones de funciones fabrica.

Que Es unique_ptr?

std::unique_ptr es un puntero inteligente que posee un objeto exclusivamente — hay exactamente un unique_ptr apuntando a cualquier objeto dado. Cuando el unique_ptr se destruye (sale del ambito), automaticamente elimina el objeto poseido.

Propiedades clave:
- No copiable — copiar crearia dos propietarios, violando la propiedad exclusiva.
- Movible — la propiedad puede ser transferida via std::move.
- Cero sobrecarga — sizeof(unique_ptr) == sizeof(T*) (con deleter por defecto). El codigo generado es identico al uso de punteros crudos con delete manual.
- Este es el puntero inteligente por defecto. Usa unique_ptr primero; solo usa shared_ptr cuando genuinamente necesites propiedad compartida.

make_unique: La Unica Forma Correcta

std::make_unique(args...) (C++14) es la unica forma correcta de crear un unique_ptr. Es seguro ante excepciones, conciso y evita escribir new.

unique_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Connection {
    std::string host;
    int port;
    Connection(std::string h, int p) : host(std::move(h)), port(p) {
        std::cout << "Connected to " << host << ":" << port << '\n';
    }
    ~Connection() {
        std::cout << "Disconnected from " << host << '\n';
    }
    void query(const std::string& sql) {
        std::cout << "Executing: " << sql << '\n';
    }
};

int main() {
    // CORRECT: use make_unique
    auto conn = std::make_unique<Connection>("localhost", 5432);
    conn->query("SELECT * FROM users");

    // Access the raw pointer (non-owning)
    Connection* raw = conn.get();  // does NOT transfer ownership

    // Transfer ownership
    auto conn2 = std::move(conn);
    // conn is now nullptr
    if (!conn) std::cout << "conn is null after move\n";

    // conn2 destroyed here — destructor called automatically
}  // Output: "Disconnected from localhost"

Deleters Personalizados para Recursos C

Muchas bibliotecas C retornan punteros crudos que deben ser liberados con una funcion especifica (no delete). unique_ptr soporta deleters personalizados para manejar estos casos. El deleter se convierte en parte del tipo, asi que tiene cero sobrecarga cuando se usa un puntero a funcion o una lambda sin estado.

custom_deleter.cpp
#include <cstdio>
#include <memory>
#include <iostream>

// Custom deleter for FILE*
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::fclose(f);
            std::cout << "File closed\n";
        }
    }
};

// Type alias for clarity
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

// Factory function
UniqueFile open_file(const char* path, const char* mode) {
    FILE* f = std::fopen(path, mode);
    if (!f) throw std::runtime_error("Cannot open file");
    return UniqueFile(f);
}

int main() {
    try {
        auto file = open_file("/tmp/test.txt", "w");
        std::fputs("Hello, RAII!\n", file.get());
        // file automatically closed when scope exits
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }

    // Lambda deleter (less common, but useful for one-offs)
    auto deleter = [](int* p) { std::cout << "custom free\n"; delete p; };
    std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
}

unique_ptr con Arrays

std::unique_ptr gestiona un array asignado dinamicamente y llama a delete[] automaticamente. Proporciona operator[] en lugar de operator* y operator->.

auto arr = std::make_unique<int[]>(100);  // 100 zero-initialized ints
arr[0] = 42;

Sin embargo, std::vector es casi siempre preferible — conoce su tamano, soporta iteracion, y es igual de eficiente. Usa unique_ptr solo cuando interfaces con APIs de C o cuando necesites un buffer no redimensionable y no copiable.

Funciones Fabrica Retornando unique_ptr

Retornar unique_ptr desde funciones fabrica es el patron idiomatico de propiedad en C++. El llamador recibe propiedad exclusiva. Si el llamador quiere propiedad compartida, puede convertir: std::shared_ptr sp = factory();.

factory_unique_ptr.cpp
#include <memory>
#include <iostream>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Factory — caller owns the result
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
    if (type == "circle")    return std::make_unique<Circle>(a);
    if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
    return nullptr;  // unknown shape
}

int main() {
    auto s = make_shape("circle", 5.0);
    if (s) {
        std::cout << s->name() << " area: " << s->area() << '\n';
    }
    // s automatically deleted here
}
Error común

Nunca pases unique_ptr por referencia const a una funcion que solo observa el objeto — pasa un puntero crudo o referencia en su lugar. Los parametros unique_ptr senalan transferencia de propiedad.

- void use(Widget* w) — observa, sin propiedad
- void sink(std::unique_ptr w) — toma propiedad (llama con std::move)
- void reseat(std::unique_ptr& w) — puede reemplazar el objeto poseido

Nunca construyas dos unique_ptr desde el mismo puntero crudo — ambos llamaran a delete, causando doble liberacion. Es por esto que se prefiere make_unique: no hay puntero crudo que reutilizar accidentalmente.

Buena práctica

1. Usa unique_ptr por defecto — deberia ser tu primera opcion para asignacion en el monton.
2. Siempre usa make_unique — nunca escribas unique_ptr(new T(...)).
3. Retorna unique_ptr desde funciones fabrica — se convierte implicitamente a shared_ptr si el llamador lo necesita.
4. Pasa por valor para transferir propiedad — void sink(unique_ptr p). El llamador debe usar std::move.
5. Pasa puntero crudo/referencia para observar — void use(const T& obj) o void use(T* obj).
6. Marca el constructor de movimiento/asignacion noexcept — critico para tipos almacenados en contenedores.

Puntos clave
  • unique_ptr proporciona propiedad exclusiva con cero sobrecarga comparado con punteros crudos
  • Siempre crea via std::make_unique(args...) — nunca uses new crudo
  • No copiable pero movible — la propiedad se transfiere via std::move
  • Los deleters personalizados habilitan RAII para recursos C (FILE*, sockets, etc.)
  • Retorna unique_ptr desde funciones fabrica — se convierte a shared_ptr si es necesario
  • Pasa puntero crudo o referencia cuando una funcion solo observa, no posee

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cual es la sobrecarga de `std::unique_ptr<T>` comparado con un `T*` crudo (con deleter por defecto)?

2. Como deberias pasar un `unique_ptr` a una funcion que necesita tomar propiedad?

3. Por que se prefiere `std::make_unique` sobre `unique_ptr<T>(new T(...))`?