std::unique_ptr
Si alguna vez has utilizado (o intentado utilizar) la clase std::auto_ptr probablemente habrás notado que es una clase rara. Pero ya no hay que preocuparse por ella porque en C++0x ha sido marcada oficialmente como obsoleta. La funcionalidad que aportaba (o lo intentaba) esta clase la proporciona con ventaja la nueva std::unique_ptr.
La clase std::auto_ptr es básicamente un wrapper alrededor de un puntero, que 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.
Por ejemplo:
1 2 3 4 5 6 7 8 9 10 11 | void funcion() { std::auto_ptr<Tipo> p( new Tipo ); //... if (cond) return; //... if (cond) throw x; //... } |
El objeto dinámico se libera siempre en cuanto se destruye el puntero, independientemente de cómo se salga del bloque en el que se ha declarado.
Otro ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Foo { private: std::auto_ptr<Tipo> m_tipo; public: Foo() { //... m_tipo.reset(new Tipo); //... if (cond) throw x; } ~Foo() { } }; |
El objeto creado en la línea 9 se libera siempre, tanto si se aborta el constructor con una excepción (no se ejecuta ~Foo(), pero sí los destructores de las variables miembro), como si se destruye el objeto normalmente.
El problema surge con el constructor de copia y el operador de asignación: se incluye un valor booleano que dice si el auto_ptr es el propietario del objeto, y solo se hace delete si este es true. Cuando se copia un auto_ptr en otro, si el primero es propietario, se le transfiere la propiedad al segundo, es decir se cambia ese flag a false en el original, y se pone a true en el destino.
Sí, has entendido bien, el constructor de copia y el operador de asignación modifican el objeto original, a pesar de que están declarados const. Esto no solo es una irregularidad importante, sino también una receta para el caos, pues en cuanto se utilizan en casos no triviales acaba resultando difícil saber cuál el el puntero propietario y cuál no. Además la implementación de la clase es absurdamente complicada para saltarse el const sin resultar en comportamiento indefinido.
Pero gracias a las referencias a R-valor, el constructor de movimiento y el operador de movimiento es posible escribir una clase que haga lo que se suponía que debía hacer auto_ptr de forma muy sencilla, y esta es la clase unique_ptr. Su definición, bastante simplificada, es parecida a lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | template <typename T> 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; }; |
Fácil, ¿verdad? Esta clase se puede utilizar en los ejemplos anteriores de auto_ptr, pero además implementa la transferencia de propiedad, sin necesidad de un flag adicional y a prueba de torpes: como la clase no es copiable es imposible hacer que un objeto de borre dos veces.
Es ideal para el tipo de retorno de una clase factory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | std::unique_ptr<Cosa> CreaCosa() { std::unique_ptr<Cosa> p(new Cosa); //... return p; } void Funcion() { std::unique_ptr<Cosa> c; //... c = CreaCosa(); //... CreaCosa(); //ops! //... } |
El objeto retornado en la línea 12 se borra al salir del bloque. El de la línea 14, sin embargo, olvidamos asignarlo a un puntero local. Pero, ¡no pasa nada! La función retorna un puntero temporal, que se destruye justo después, borrando el puntero.
La función miembro release devuelve el puntero interno, y luego lo olvida. Resulta especialmente útil para no perder objetos en el caso de que ocurran excepciones:
class Uno { /*...*/ }; class Dos { /*...*/ }; class Tres { /*...*/ }; struct Objetos { Uno *uno; Dos *dos; Tres *tres; }; void CreaObjetos(Objetos *objs) { std::unique_ptr<Uno> uno(new Uno); //cosas que pueden lanzar std::unique_ptr<Dos> dos(new Dos); //más cosas que pueden lanzar std::unique_ptr<Tres> uno(new Tres); //aún más cosas que pueden lanzar //estas ya no lanzan objs->uno = uno.release(); objs->dos = dos.release(); objs->tres = tres.release(); }
Hacer esto bien, cubriendo todos los posibles casos de excepciones, sin usar unique_ptr o similar, no es un trabajo trivial, e implicaría añadir un buen número de try/catch. Este código, sin embargo es muy fácil de entender: los unique_ptr sirven de red de seguridad, para recoger los objetos que caigan si el código falla.
Variantes
Esta clase permite también indicar un borrador personalizado, que se utilizará en lugar de delete. El borrador debe ser un puntero a función o un objeto de una clase que implemente el operator(), recibiendo como paránetro el puntero a T. El inconveniente es que el tipo del borrador se debe indicar como argumento del template.
El ejemplo de libro es:
struct FileDeleter { void operator()(FILE *f) { fclose(f); } }; typedef std::unique_ptr<FILE, FileDeleter> FilePtr; void Funcion() { FilePtr f(fopen("f.txt", "r")); //... }
También podría hacerse con un puntero a función:
typedef std::unique_ptr<FILE, int (*)(FILE*)> FilePtr; void Funcion() { FilePtr f(fopen("f.txt", "r"), &fclose); //... }
Pero es menos portable, porque no tenemos la seguridad de que el puntero a fclose tenga el tipo que nosotros creemos (podría tener parámetros ocultos, o atributos especiales, o se una macro).
Existe también un unique_ptr para arrays, que llama debidamente a delete[]. Simplemente se usa la especialización con corchetes:
void Funcion() { std::unique_ptr<Tipo[]> p(new Tipo[10]); }
Esta especialización sobrecarga el operator[](int) en lugar de los de referencia de punteros.
Artículos relacionados:
- La regla de los cuatro miembros y el principio RAII La regla de los cuatro miembros no se refiere a...
- Referencias a R-valor (1ª parte) En un post anterior comentaba que en C++ no se...
RSS
Deja un comentario