Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.12.2b1   Punteros inteligentes

§1  Preámbulo

Al presentar el operador new ( 4.9.20), se insistió en el peligro que supone la permanencia de los objetos creados, junto al hecho de que sus referentes (los punteros que señalan a esos objetos), sean a su vez objetos automáticos que pueden ser destruidos inadvertidamente, tanto por "olvidos" del programador como por acción del mecanismo de excepciones.

Para evitar este tipo de problemas se ha incluido en la Librería Estándar STL ( 5.1) una plantilla especial denominada auto_ptr, con la que generar punteros inteligentes. Esto es: punteros que destruyen automáticamente el objeto señalado cuando ellos mismos salen de ámbito.

Nota: en el capítulo dedicado a objetos-puntero ( 4.13.3), puede encontrarse una descripción más detallada de los principios teóricos utilizados.

Esta plantilla, definida en el fichero <memory> [1], responde a la siguiente declaración:

template <class X> class auto_ptr;

§2  Descripción

La plantilla auto_ptr puede instanciar un objeto que contenga un valor nulo, o la dirección de un objeto creado con new; encargándose de destruirlo automáticamente, cuando él mismo sea destruido (por ejemplo por salir de ámbito).

Hay que advertir que estos objetos no son punteros en el sentido formal.  Es decir, en la instanciación concreta:

auto_ptr<tipoX> obj;

obj no es un objeto tipoX* (puntero a objeto tipo X), sino auto_ptr<tipoX>. Sin embargo, funcionalmente se comportan en parte como punteros [2], en el sentido que pueden contener la dirección de un objeto, y pueden aplicársele los operadores de indirección * y selector indirecto ->. En otros aspectos. Por ejemplo, frente al operador de asignación, su comportamiento es un tanto singular.

La base de su funcionamiento consiste en que los objetos automáticos (como estos punteros) creados en un ámbito, son automáticamente destruidos por el compilador cuando salen de él. Para esto el compilador invoca los destructores adecuados. En caso de objetos auto_ptr, el destructor se define de forma que destruya también el objeto señalado.

Este comportamiento permite que los objetos creados con new puedan coexistir de forma segura con el mecanismo de excepciones, ya que el proceso de limpieza de la pila ("Stack unwindig 1.6) garantiza la destrucción de los objetos persistentes involucrados.

Para que este comportamiento sea eficaz, la clase auto_ptr contiene un indicador de propiedad. De forma que los objetos señalados se consideran una especie de "propiedad" del puntero. Un objeto puede ser señalado de forma segura por un puntero auto_ptr, que detenta entonces su "propiedad";  pero la copia de este puntero no solo produce un clon del anterior; también le transfiere la propiedad del objeto señalado en caso que el primero no fuese un puntero nulo.

Nota: existe una técnica similar, que también persigue la destrucción automática de los objetos referenciados cuando las referencias salen de ámbito, que utiliza un procedimiento distinto; se denomina de contador de referencias ("Reference counting"). En esta técnica se ha eliminado el concepto de indicador de propiedad, de forma que varios objetos-puntero pueden señalar al mismo objeto persistente. Para evitar que cuando uno de estos objetos salga de ámbito destruya el objeto, si existen otros que todavía lo señalan (lo que produciría punteros descolgados), el objeto referenciado mantiene un contador de los punteros que lo referencian. Cuando un objeto-puntero sale de ámbito, su destructor consulta el contador y lo disminuye en una unidad. Solo si el contador llega a cero, el objeto referenciado es destruido.

§3  Interfaz

La plantilla auto_ptr tiene la siguiente interfaz:

template <class X> class auto_ptr {
  template <class Y> class auto_ptr_ref { // puntero subyacente (privado)
    public:
    const auto_ptr<Y>& p;
    auto_ptr_ref (const auto_ptr<Y>&);
  };
  public:
  typedef X element_type;
  explicit auto_ptr (X* = 0) throw();     // constructor explícito por defecto
  auto_ptr (const auto_ptr<X>&) throw();  // constructor-copia
  template <class Y>                       // constructor
     auto_ptr (const auto_ptr<Y>&) throw();
  auto_ptr<X>& operator=(const auto_ptr<X>&) throw(); // función-operador
  template <class Y>                       // función operador
     auto_ptr<X>& operator= (const auto_ptr<Y>&) throw();
  ~auto_ptr ();                            // destructor
  X& operator* () const throw();          // función-operador
  X* operator-> () const throw();         // función-operador
  X* get () const throw();                // método
  X* release () throw();                   // método
  void reset (X*=0) throw();              // método
  auto_ptr(auto_ptr_ref<X>) throw();      // constructor
  template <class Y>
    operator auto_ptr_ref<Y>() throw();
  template <class Y>
    operator auto_ptr<Y>() throw();
};

§3.1  Descripción

Lo primero que sorprende, es que esta plantilla tiene un primer (y único), miembro privado auto_ptr_ref, que es a su vez otra plantilla.  Este miembro tiene a su vez dos miembros públicos: una referencia a un objeto auto_ptr, y un constructor que recibe como argumento una referencia a un objeto tipo auto_ptr.

El objeto de este miembro es almacenar una referencia a un objeto auto_ptr que solo pueda ser construida dentro de un objeto auto_ptr utilizando una referencia a auto_ptr. El objeto referenciado por este miembro se denomina indicador de propiedad o puntero subyacente, y se utiliza en el mecanismo de copia de objetos de esta clase, en el que se transfieren también los "derechos de propiedad" sobre el objeto señalado.

Además del anterior, la clase tiene un destructor, cuatro constructores (uno de ellos por defecto y otro explícito) y tres funciones miembro. También se definen las siguientes funciones operador para que puedan ser aplicadas con miembros de la clase: operador de indirección *; selector indirecto ->; el de asignación =.

§3.2  Miembros

Aparte del indicador de propiedad ya mencionado, la clase dispone de los siguientes miembros públicos:

  Constructores y destructores:
  • explicit auto_ptr (X* p = 0) throw();

    Este constructor explicit ( 4.11.2d1) se encarga de construir un objeto de la clase auto_ptr<X>, inicializando el valor señalado a p, y adquiriendo la propiedad del objeto.  Por supuesto p debe señalar a un objeto de la clase X, o de una subclase para la que esté definida y sea accesible la operación delete p. p puede ser también un puntero nulo ( 4.2.1).

  • auto_ptr (const auto_ptr<X>& a) throw();
    template <class Y> auto_ptr (const auto_ptr<Y>& a) throw();

    Estos constructores-copia construyen un objeto de la clase auto_ptr<X>, y copian el argumento a (que es la referencia a un objeto auto_ptr) a *this (el nuevo objeto). Si el objeto a posee el puntero subyacente, entonces el nuevo objeto se transforma en el nuevo propietario.

  • auto_ptr (const auto_ptr_ref<X> r) throw();

    Construye un objeto auto_ptr a partir del miembro privado auto_ptr_ref.

  • ~auto_ptr ();

    Destructor de la clase que incluye una llamada a delete para destruir el objeto señalada por el puntero subyacente.

Observe que los constructores se han declarado de forma que no puedan lanzarse excepciones desde su interior ( 1.6.4).

  Operadores:
  • auto_ptr<X>& operator= (const auto_ptr<X>& a);
    template <class Y> auto_ptr<X>& operator= (const auto_ptr<Y>& a);

    Estas funciones-operador (una de ellas genérica) definen versiones particulares del operador de asignación = para miembros de la clase auto_ptr. Copian el argumento a a *this. Si a es propietario del puntero subyacente, entonces *this se convierte en el nuevo propietario. Si *this ya era el propietario de un puntero, entonces el objeto asociado es destruido antes de asignársele la nueva propiedad.

    Tienen las definiciones siguientes:

    auto_ptr<X>& operator= (auto_ptr<X>& a) throw() {
      reset(a.release());
      return *this;
    }

    template <class Y>
      auto_ptr<X>& operator= (auto_ptr<Y>& a) throw() { 
        reset(a.release());
        return *this;
    }

  • X& operator* () const;

    Esta función sobrecarga el operador de indirección * para objetos auto_ptr. Aplicado a un objeto devuelve una referencia al objeto señalado por el puntero subyacente.

  • X* operator-> () const;

    Esta función sobrecarga el selector indirecto de miembro -> para objetos de la clase. La aplicación de este opeador a un objeto auto_ptr devuelve el puntero subyacente.

  • template <class Y> operator auto_ptr_ref<Y> ();

    Operador que construye objetos de la sub-clase. Construye un objeto auto_ptr_ref a partir de *this.

  • template <class Y> operator auto_ptr<Y> ();

    Operador que construye un nuevo objeto auto_ptr a partir del puntero subyacente del objeto señalado por *this. Esta función realiza una invocación al método release , de forma que a partir de entonces el objeto *this no es propietario del puntero. La función devuelve un nuevo objeto auto_ptr.

  Métodos:
  • X* get () const;

    Este método devuelve el puntero subyacente.

  • X* release();

    Deshace la propiedad del objeto sobre el puntero subyacente y devuelve su valor.

  • void reset(X* p)

    Este método es en cierta forma complementario del anterior. Establece el valor del puntero subyacente del objeto al valor p suministrado, borrando el valor anterior.

§4  Utilización

Generalmente la utilización práctica de objetos auto_ptr se limita a su uso como punteros de los objetos creados con new. Es rara la utilización explícita de sus miembros públicos, y la asignación explícita que podíamos llamar clásica:

class C {...};
...
auto_ptr<C> ptr = new C;

presenta alguna dificultad.  Como se ha señalado , los miembros de la asignación no son del mismo tipo, por lo que el intento anterior produce un error de compilación:  Cannot convert 'C *' to 'auto_ptr<C>'. A esto se suma que los compiladores actuales no permiten un modelado con plantillas, de forma que tampoco funciona un intento del tipo:

auto_ptr<C> ptr = static_cast<auto_ptr<C>> (new tipoC);


En la práctica, para realizar la asignación debe utilizarse una invocación implícita o explícita al constructor de auto_ptr ( 4.11.2d3) con el argumento adecuado. De esta forma el constructor se encarga de iniciar el puntero al objeto:

auto_ptr<C> ptr(new C);                // Ok. invocación implícita
auto_ptr<C> ptr = auto_ptr<C>(new C);  // Ok. invocación explícita

Ejemplo

#include <iostream>
#include <memory>           // para usar auto_ptr
using namespace std;

struct C {                  // una clase
  int mi;
  int getmi() { return mi; }
  C (int i = 0) : mi(i) { } // constructor por defecto
};

int main () {               // ===================
  auto_ptr<C> pi = auto_ptr<C>(new C(12345));      // M.1
  cout << "1 Valor mi: " << (*pi).mi << endl;      // M.2
  cout << "2 Valor mi: " << pi->mi << endl;        // M.3 
  cout << "3 Valor mi: " << (*pi).getmi() << endl; // M.4
  cout << "4 Valor mi: " << pi->getmi() << endl;   // M.5
  cout << "5 Valor mi: " << pi.get() << endl;      // M.6

  auto_ptr<C> pi2 = pi;     // M.7
//cout << "6 Valor mi: " << pi->getmi() << endl;  Error!!
  cout << "7 Valor mi: " << pi2->getmi() << endl;  // M.9

    return 0;                // el objeto es destruido
}

Salida:

1 Valor mi: 12345
2 Valor mi: 12345
3 Valor mi: 12345
4 Valor mi: 12345
5 Valor mi: 00672E18
7 Valor mi: 12345

Comentario

En M.1 se crea un objeto con new al que denominaremos ob (en realidad no tiene nombre). El puntero resultante de new es aplicado al constructor de auto_ptr, creándose un objeto temporal que señala a ob y es su propietario. Simultáneamente en el lado izquierdo de la asignación se crea un puntero inteligente pi de tipo auto_ptr<C>, al que mediante el constructor-copia se le asigna el contenido del objeto temporal. A partir de este momento pi es el nuevo propietario de ob.

pi actúa como puntero del objeto y se le pueden aplicar los operadores de indirección * y selector indirecto ->. En M.2 se obtiene el miembro mi, que es público, señalando el objeto ob mediante la indirección de su puntero. En M.3 se obtienen el mismo resultado con una notación más ortodoxa que utiliza el selector indirecto ( 4.2.1f).

La sentencias M.4 y M.5 son similares a las anteriores. Ahora se invoca el método getmi del objeto, que también es público, utilizando ambas notaciones.

En M.6 se invoca el método get del objeto pi. El resultado es una dirección de memoria (recordemos que este método devuelve el puntero subyacente).

La sentencia M.7 define un nuevo objeto auto_ptr pi2, al que se asigna el valor del puntero anterior. El operador de asignación se encarga de transferirle la propiedad del objeto ob, de forma que en adelante el puntero inicial pi se convierte en un puntero nulo.

La sentencia M.8 produce un error de runtime, consecuencia del intento de deferenciar un puntero nulo ( 4.2.1). En cambio, un intento análogo en M.9 con el nuevo propietario pi2, proporciona el resultado apetecido.

En la sentencia M.9 en que ocurre el abandono del ámbito de main, se produce la destrucción de todos los objetos automáticos, incluyendo el puntero pi2 y el objeto asociado ob.

  Tema relacionado
  • Control de recursos ( 4.1.5a)

  Inicio.


[1]  Se considera incluida en el conjunto de utilidades de Librería Estándar concernientes al manejo de memoria.

[2]  No están definidos con ellos todas las operaciones que pueden efectuarse con los punteros formales ( 4.2.2).