Conteo de Referencias

 
 
Clasificación Intención Otros nombres Motivación Aplicabilidad Estructura Participantes
Colaboraciones Consecuencias Implementación Patrones Relacionados  
 

Clasificación

Idioms en C++
 

Intención

Eliminar copias físicas innecesarias de objetos. 
 

Otros nombres

Reference Counting
 

Motivación

Retomando el ejemplo de la clase String en la forma canónica de una clase, se implementaba una copia física en el constructor por copia y en la operación de asignación entre Strings. Esta implementación hace que cada vez que se pase un String como argumento de una función o en operaciones de asignación se realice una nueva copia de la representación, en algunos casos innecesaria, por la misma manipulación de los objetos. 

Se busca compartir la representación de las cadenas siempre que sea posible, eliminando el concepto de copia física por el de copia lógica, donde varios objetos comparten la misma representación mientras permanezcan sin cambio.

 

Aplicabilidad

Use este idiom para clases que son frecuentemente copiadas por asignación o paso de parámetros, partiuclarmente si los objetos son grandes o complejos y la copia es costosa en tiempo y espacio de memoria. Uselo cuando varias copias lógicas del objeto puedan existir, donde los datos no requieran ser copiados; por ejemplo, un objeto que represente una ventana que es pasado entre funciones o información compartida por varios manejadores, donde cada uno de ellos quiera una copia para acceder al recurso.

Estructura

Se retoma la estructura del idiom Manija - Cuerpo para representar la copia compartida (Cuerpo) y los distintos administradores (Manijas). Por ejemplo, la estructura de objetos para las siguientes líneas de código 
 
 
String a = "hola mundo";
String b = a;
 

sería 

Objetos en una relación de conteo de referencias
 
 

Participantes

  • El objeto manija que comparte la representación con las otras copias
  • El objeto cuerpo que representa la cadena real

Colaboraciones

El objeto cuerpo guarda el conteo de referencias y provee operaciones para manipularlo: agregar una nueva referencia, borrar una y desaparecer cuando ya no existan referencias al objeto.
 

Consecuencias

  1. Copiar un objeto es más rápido, porque la copia es lógica y se comparte la misma representación
 

Implementación

Se presentan tres formas de implementar este Idiom: 

1. Forma general, derivada del Idiom Manija Cuerpo

En esta forma la clase Cuerpo es la anterior Clase String (ahora denominada StringRep) que se encarga de la manipulación de la cadena; mientras que la clase Manija se encarga de la administración de la memoria. La implementación correspondiente es como sigue 
 
 
class StringRep
{
friend String
private:           // Solo accesible desde String
  StringRep( ); // Constructor sin argumentos
  StringRep( const StringRep& ); // Constructor por copia
  StringRep& operator=( const StringRep& ); // asignación
  ~StringRep( ); // Destructor
  // ... Otras operaciones del tipo
  StringRep operator+ (const StringRep& s) const;
private:
  char* rep;  // Implementación del tipo.
  int count;  // COnteo de referencias
};
class String
{
public:
  String( ) // Constructor sin argumentos
  {
    rep = new StringRep; rep->count = 1;
  }
  String( const String& s) // Constructor por copia
  {
    rep = s.rep; rep->count++;
  }
  String& operator=( const String& s ) // asignación
  {
    s.rep->count++;
    if( --rep->count <= 0 ) delete rep;
    rep = s.rep;
    return *this;
  }
  ~String( ) // Destructor
  {
    if( --rep->count <= 0 ) delete rep;
  }
  // ... Otras operaciones del tipo
  String operator+ (const String& s) const
  {
    StringRep y = *rep + *s.rep;
    return String( y.rep );
  }
private:
  StringRep* rep;  // Implementación del tipo.
};
 

En esta representación se deja todo el conocimiento de la forma de manipulación de cadenas en la clase interna (StringRep) y el manejo de referencias y manejo de memoria en la externa (String). Sin embargo, tiene aún problemas de eficiencia, en particular en la operación de suma de cadenas. 
 

2. Una implementación más eficiente

En este caso se soluciona el problema de eficiencia del caso anterior. En esta opción se dejan las operaciones en la clase Manija (String) y se limita la clase Cuerpo (StringRep) a las operaciones de creación y destrucción. El código es como sigue 
 
 
#include <string.h>
class StringRep
{
  friend class String;
private:
  typedef char* Char_p;
  StringRep( StringRep::Char_p* const r)
  {
    rep = *r: *r=0; count =1;
  }
public:
  StringRep( const char* s )
  {
    ::strcpy(rep=new char[::strlen(s)+1], s ); count =1;
  }
  ~StringRep() 
  {
    delete[] rep;
  }
private:
  char* rep;
  int count;
};
class String
{
public:
  String( )
  {
    rep = new StringRep("");
  }
  String( const Strig& s )
  {
    rep = s.rep; rep->count++;
  }
  String& operator=( const String& s ) // asignación
  {
    s.rep->count++;
    if( --rep->count <= 0 ) delete rep;
    rep = s.rep;
    return *this;
  }
  ~String( ) // Destructor
  {
    if( --rep->count <= 0 ) delete rep;
  }
  String( const char* s )
  {
    rep = new StringRep(s);
  }
  String operator+( const String&) const;
  int length() const
  {
    return ::strlen(rep->rep);
  }
private:
  String(StringRep::Char_p* const r )
  {
    rep = new StringRep(r);
  }
  StringRep* rep;
};
String String::operator+ (const String& s ) const
{
  char* buf = new char[s.length() + length() + 1];
  ::strcpy(buf, rep->rep);
  ::strcat(buf, s.rep->rep);
  String retval(&buf); // call the new private constructor
  return retval;
}

 
En esta implementación se alijera el proceso de creación de Strings en la operación suma. Se reutiliza la memoria creada en esta operación como base de la implementación del nuevo String. Una desventaja es la falta de separación entre las funciones de cada una de las clases.

3. Contador de Apuntadores

En esta representación se usa la sobrecarga del operador ->. Por medio de esta sobrecarga se permite a un usuario de la clase Manija acceder a los servicios públicos de la clase Cuerpo
 
 
class StringRep {
friend String;
private:
  StringRep ()  
  {
    *(rep=new char[1]) = '\0'; 
  }
  StringRep (const StringRep& s)
  {
    ::strcpy(rep=new char[::strlen(s.rep)+1],s.rep);
  }
  ~StringRep()  
  {
    delete[] rep;
  }
  StringRep(const char *s) 
  {
     ::strcpy(rep=new char[::strlen(s)+1],s);
  }
  String operator+(const String& s) const {
    char *buf = new char[s->length() + length() +1];
    ::strcpy(buf,rep);
    ::strcat(buf,s->rep);
    String retval(&buf);
    return retval;
  }
  int length() const
  {
    return ::strlen(rep);
  }
  void print() const
  {
    ::printf("%s\n", rep);
  }
private:
  StringRep( char** const r)
  {
    rep = *r: *r=0; count=1;
  }
  char* rep;
  int count;
};
class String
{
friend class StringRep;
public:
  String operator+( const String& s) const
  {
    return *p + s;
  }
  StringRep* operator->() const
  {
    return p;
  }
  String()
  {
    (p=new StringRep())->count = 1;
  }
  String(const String& s )
  {
    (p = s.p)->count++;
  }
  String( const char* s)
  {
    (p = new StringRep(s))->count = 1;
  }
  String operator=( const String&q)
  {
    if( --p->count <= 0 && p != q.p)
      delete p;
    (p=q.p)->coount++;
    return *this;
  }
  ~String()
  {
    if(--p->count <=0)
      delete p;
  }
private:
  String( char** r )
  {
    p = new StringRep(r);
  }
  StringRep *p;
};

En este caso la clase String sigue administrando la memoria y la clase StringRep las operaciones de una cadena, sin embargo, estas ya no se reflejan en String sino que se acceden mediante el operador ->. Se separan claramente las funciones, pero es necesario utilizar la simbología de un apuntador sobre el objeto String para acceder a sus servicios.

Esta implementación es útil para no tener que duplicar cambios en la clase Manija causados en la clase Cuerpo.
 

 
 

Patrones relacionados

Manija - Cuerpo