Repaso a C++11: Punteros std::unique_ptr

Repaso a C++11: Punteros std::unique_ptr
Sin comentarios Facebook Twitter Flipboard E-mail

Una de las cosas más temidas y engorrosas de C++ siempre han sido los punteros, el tener que gestionar manualmente la liberación de memoria hace que a veces pueda haber fugas de memoria o errores no esperados por usar un puntero al que hemos hecho delete, pero no hemos eliminado. Ahora con C++11 tenemos los punteros std::unique_ptr que gestionan automáticamente la memoria cuando son destruidos.

Antes de C++11, en las versiones prelimanares del nuevo estándar el llamado C++0x se crearon los punteros std::auto_ptr que debían ser unos punteros dinámicos para C++, pero ya han sido marcados como obsoletos y están fuera del nuevo estándar. La función que hacían antes ahora la cumplen los unique_ptr.

El problema de auto_ptr

La clase auto_ptr es básicamente un wrapper sobre un puntero donde éste es un objeto miembro y se llama a delete en el destructor. Esto resulta útil para la gestión automática de objetos dinámicos, tanto en contextos locales como en miembros de una clase.

void funcion()
{
    std::auto_ptr p( new Tipo );
    //...
    if (cond)
        return;
    //...
    if (cond)
        throw x;
    //...
}

El objeto dinámico se libera siempre independientemente de como se salga de la función ya que el destructor del objeto es llamado y por tando al delete del puntero.

class Foo
{
private:
    std::auto_ptr m_tipo;
public:
    Foo()
    {
        //...
        m_tipo.reset(new Tipo);
        //...
        if (cond)
            throw x;
    }
    ~Foo()
    {
    }
};

El objeto miembro m_tipo se libera siempre ya que aunque salgamos con alguna exepción y por tanto nunca se ejecute el destructor ~Foo() sí que se llama al destructor de las variables miembros del objeto y por tanto se libera siempre el puntero.

La clase parece que funciona bien entonces y que cumples su función correctamente. El problema es cuando se intenta usar el constructor copia o el operador de asignación. Los punteros auto_ptr tienen un flags booleano que marca si es el propietario o no del objeto y solo el propietario puede eliminar el puntero. El problema es que al asignar un puntero a otro ya sea con el constructor copia o con el operador de asignación en el puntero de origen se pone éste flags a false y a true en el puntero que lo recibe.

Como sabrás los métodos de constructor copia y operador de asignación deben ser declarados como constantes y no deben modificar nunca el objeto de origen, pues bien, auto_ptr se las ingenia para modificar el flags de propiedad del objeto de origen y ponerlo a false siendo esto una irregularidad en toda regla del lenguaje.

Este sin sentido lo arregla la clase unique_ptr que aprovecha las nuevas características del lenguaje con las referencias R-Value y evita la copia de punteros en favor del movimiento, evitando así tener más de un propietario y lío que monto auto_ptr. Una aproximación simplificada a esta nueva clase sería algo así:

template  class unique_ptr
{
public:
    explicit unique_ptr(T *p)
        :m_ptr(p)
    {
    }
    unique_ptr()
        :m_ptr(NULL)
    {
    }
    //el destructor
    ~unique_ptr()
    {
        delete m_ptr;
    }    
    //no es copiable
    unique_ptr(const unique_ptr &o) = delete;
    unique_ptr &operator=(const unique_ptr &o) = delete;
    //pero sí es movible
    unique_ptr(unique_ptr &&o)
    {
        m_ptr = o.m_ptr;
        o.m_ptr = NULL;
    }
    unique_ptr &operator=(unique_ptr &&o)
    {
        delete m_ptr;
        m_ptr = o.m_ptr;
        o.m_ptr = NULL;
    }
    //otros
    T *get() const
    {
        return m_ptr;
    }
    void reset(T *p)
    {
        T *t = m_ptr;
        m_ptr = p;
        delete t;
    }
    T *release()
    {
        T *t = m_ptr;
        m_ptr = NULL;
        return t;
    }
    T *operator->() const
    {
        return m_ptr;
    }
    T &operator*() const
    {
        return *m_ptr;
    }
private:
    T *m_ptr;
};

Como vemos hacemos que la clase no sea copiable, pero si que sea movible. Además evitamos usar fácilmente el uso de flags adicionales haciendo un código mucho más elegante.

Esto es muy útil para patrones de diseño como el factory.

std::unique_ptr CreaCosa()
{
    std::unique_ptr p(new Cosa);
    //...
    return p;
}
void Funcion()
{
    std::unique_ptr c;
    //...
    c = CreaCosa();
    //...
    CreaCosa(); //ops!
    //...
}

En este ejemplo se crean dos objetos en una al asignarlo a la variable c usamos el operador de movimiento desde el puntero interno al local sin realizar una copia de más. En el segundo caso se crea el objeto temporal, pero como no está asignado a ninguna variable se destruye y se libera la memoria correctamente sin ninguna intervención.

Comentarios cerrados
Inicio