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]


5.1.5  Asignadores de memoria

§1 Antecedentes

Para comprender las características y utilidad de los asignadores ("Allocators"), hay que recordar lo indicado en la introducción de este tutorial ( 0.I), donde afirmábamos que existen versiones de C++ para prácticamente todas las plataformas hardware y Sistema Operativo conocidos. Es significativo que las diferentes combinaciones de máquinas y SOs utilizan distintos modelos de memoria, a pesar de lo cual, C++ utiliza un modelo único cuyos detalles de implementación están encapsulados en el compilador, de forma que el usuario no tiene que preocuparse de ellos. Por ejemplo, la diferencia entre un puntero señalando al elemento enésimo de una matriz y el que señala al primer elemento es siempre un entero n, con independencia del modelo de memoria de la máquina utilizada.

En C++ existen dos mecanismos relacionados con la gestión de memoria: el que se ocupa del manejo de los objetos automáticos ( 4.1.5) y el que se ocupa de la memoria dinámica.  El primero, que maneja la pila, está inextricablemente unido con el propio lenguaje (recordemos que C y C++ son lenguajes orientados a la pila) y ha sido descrito en el apartado correspondiente ( 1.3.2). El segundo, que entiende de la asignación ("allocation") de memoria a los objetos persistentes, y su desasignación o liberación ("free") cuando ya no son necesarios, es en cierta forma un "añadido" al propio lenguaje, y de hecho, su interfaz para el programador (como lo utiliza), ha variado a lo largo del tiempo (se incluyó una descripción al respecto al tratar del operador new 4.9.20).

Cuando se diseñó la STL, y debido precisamente a los distintos modelos de memoria a los que debería adaptarse, los miembros del Comité ya tenían en mente la previsible dificultad para implementar la enorme cantidad de código que constituía la nueva librería [1]. Con el fin de facilitar su implementación en los distintos compiladores (uno de los objetivos preferentes del Comité de Estandarización C++ es facilitar la portabilidad del lenguaje a las diversas plataformas), los puntos en que se hacía referencia al manejo de memoria se redujeron al mínimo, concentrándolos en una clase que se encargaría de asignar y desasignar memoria para todos los demás algoritmos que lo precisaran. El resultado es que los objetos de esta clase (allocators) permiten desvinculan la STL de los aspectos relativos al modelo de memoria utilizado por cada máquina concreta.

§2 Asignadores

Habida cuenta que la misión principal de los contenedores es el almacenamiento de objetos, resulta natural que entre sus funciones más importantes, se encuentre la gestión del espacio necesario para albergarlos. Esta gestión no se limita a asignar y liberar el espacio necesario para sus miembros. En ocasiones también debe gestionar espacio para la estructura de índices que mantiene el orden de la secuencia. Por ejemplo, cuando añadimos miembros a un contenedor tipo vector ( 5.1.1c1), este se redimensiona automáticamente conforme se van añadiendo nuevos elementos.  Análogamente, cuando añadimos nuevos elementos a un map ( 5.1.1e4) este acomodo afecta a los nuevos elementos y a las claves asociadas que constituyen su sistema de índice.

Como se ha dicho, todas las entidades de la STL que precisan de memoria dinámica lo hacen a través de los "servicios" de una clase "asignadora" que es utilizada como argumento (recuerde que las entidades de la STL son plantillas que aceptan distintos parámetros). Debido a que además de portable, la STL es extensible, existe libertad para que el usuario implemente su propio asignador de memoria (ver nota ); aunque la STL proporciona un gestor por defecto, la clase allocator, que implementa la funcionalidad requerida mediante la utilización de los operadores estándar new y delete [2]. En caso de no indicarse otro explícitamente, se utiliza este asignador por defecto, que suele ser suficiente en la mayoría de los casos. El resultado es que el usuario puede despreocuparse de la cuestión, y que los asignadores son ráramente utilizados de forma explícita. Por ejemplo, la definición del contenedor vector es del siguiente tenor:

namespace std {
  template <class T, class Allocator = allocator<T> > class vector {
    public:

    ...
  };

}


Desde la perspectiva del usuario de un contenedor estándar, el manejo de memoria es realizado automáticamente mediante el parámetro de moldeo.  Por ejemplo, para definir una lista de enteros myList utilizando el asignador por defecto, puede utilizarse el parámetro <int> y escribir:

#include <memory>

...

list <int> myList;


En la declaración del contenedor también puede proporcionarse un asignador específico mediante un segundo parámetro de moldeo. Por ejemplo, para utilizar en la lista anterior un asignador propio denominado fastAllocator, que suponemos está pensado para tipos int, y definido en el fichero de cabecera <fastAllocator.h>, utilizaríamos la definición siguiente:

#include <memory>

#include <fastAllocator.h>

...

typedef list <int, fastAllocator> myList;


También podría utilizarse el asignador por defecto de forma explícita:

#include <memory>

...

allocator<int> miAllocInt;

list <int, miAllocInt> myList;


Como puede verse, cuando se instancia una especialización concreta de un contenedor genérico, debe especificarse el tipo de miembro que alojará en el contenedor y el asignador de memoria que utilizara. Posteriormente, cuando se instancie un objeto del tipo myList, se especifica el objeto-asignador que utilizará el contenedor:

fastAllocator asignador1;

myList lista1(asignador1);

Nota: el autor del lenguaje [3] nos informa que un "allocator" es una abstracción utilizada para aislar los detalles del manejo de memoria dinámica de los algoritmos y contenedores que deben utilizarla, y que cualquier cosa que se comporte como un allocator es un allocator. Definir un asignador propio es un proceso relativamente simple ( xxx).


Debemos observar que en el diseño actual de la STL, los asignadores son clases genéricas (plantillas), y que una instancia de dicha plantilla asigna memoria para objetos de un tipo específico T. En consecuencia, cuando proporcionamos un allocator específico a un contenedor, su tipo debe coincidir con el de los miembros del contenedor.

A este respecto señalemos que el Estándar establece que condiciones debe cumplir una clase M para que pueda ser considerada un allocator y utilizada por las entidades de la STL que lo necesiten. En concreto establece que debe tener ciertas propiedades cuyos nombres y significados están determinados, así como ciertos métodos cuyos nombres y comportamiento están igualmente especificados. Entre ellos podríamos destacar los siguientes:

Expresión Valor devuelto Comentario
M::pointer Puntero a tipo T.  
M::size_type Entero sin signo Un tipo que puede representar el tamaño del mayor objeto en el modelo de memoria utilizado.

M::difference_type

Entero con signo Tipo que puede representar la diferencia entre dos punteros cualesquiera en el modelo de memoria utilizado.

a.allocate(n)

a.allocate(n,u)

M::pointer Asigna memoria para n objetos de tipo T (sin que los objetos sean construidos). En caso de fallo debe lanzar una excepción.
a.deallocate(p,n)   Desasigna la memoria para n objetos a partir de la dirección señalada por el puntero p.  La memoria debe haber sido asignada previamente con allocate(), y los objetos contenidos en ella haber sido destruidos previamente.
a1 == a2 bool La clase M debe terner definido el método operator== de forma que devuelva true si el almacenamiento asignado desde uno puede ser desasignado desde el otro.
a1 != a2 bool Equivalente a: !(a1 == a2)

Las expresiones utilizan los significados siguientes:

T   un tipo cualquiera

M  una clase-asignador para el tipo T

n   un valor de tipo M::size_type

a, a1, a2  objetos de tipo M&


En el cuadro de condiciones anterior se observa que los asignadores no crean los objetos. Se limitan a asignar memoria al más puro estilo de las funciones de librería clásica calloc() y malloc(). Lo que realmente caracteriza a esta clase son los métodos allocate() y deallocate() que, en cierta forma, representan la contrapartida de las funciones malloc() y free() de la librería clásica.

La STL incluye también algunos algoritmos para manipular la memoria no inicializada.

El funcionamiento del sistema se basa en que cada vez que es creado un contenedor que necesita manejo de memoria, recibe un asignador. De esta forma, cada vez que el contenedor necesita asignar o rehusar memoria no necesita conocer ningún detalle sobre el modelo de memoria de la máquina, ya que utiliza el asignador para estos menesteres.

§3 El manejador por defecto

Las definiciones de los asignadores de la STL están en el espacio de nombres std y se encuentran agrupadas en la cabecera <memory>.  La STL proporciona un manejador por defecto denominado allocator.  Esta clase es en realidad una plantilla que puede instanciarse para manejar cualquier tipo. Responde a la siguiente interfaz:

template <class T> class allocator {
  public:
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
  typedef T* pointer;
  typedef const T* const_pointer;
  typedef T& reference;
  typedef const T& const_reference;
  typedef T value_type;
  template <class U> struct rebind { typedef allocator<U> other; };
  allocator() throw();
  allocator(const allocator&) throw();
  template <class U> allocator(const allocator<U>&) throw();
  ~allocator() throw();
  pointer address(reference x) const;
  const_pointer address(const_reference x) const;
  pointer allocate(
          size_type, allocator<void>::const_pointer hint = 0);
  void deallocate(pointer p, size_type n);
  size_type max_size() const throw();
  void construct(pointer p, const T& val);
  void destroy(pointer p);
};

Por supuesto que la clase satisface las premisas indicadas por el Estándar para ser un allocator (apuntadas en el epígrafe anterior ) por lo que es un asignador estándar. También se proporciona una instanciación para el tipo void que responde a la siguiente interfaz:

template <> class allocator<void> {
   public:
   typedef void* pointer;
   typedef const void* const_pointer;
// reference-to-void members are impossible.
   typedef void value_type;
   template <class U> struct rebind { typedef allocator<U> other; };
};

Ejemplos

vector<double> V(100, 5.0);     // Usa el asignador por defecto
vector<double, single_client_alloc> local(V.begin(), V.end());


  Inicio.


[1] En realidad, lo que hizo el Comité fue adoptar con algunos retoques la librería diseñada por Alexander Stepanov y Meng Lee en los laboratorios de Hewlett-Packard e incluirla como parte de la Librería Estándar C++.

[2] Observe que el Estándar declara que las funciones operator new y operator new[ ] son funciones de asignación ("Allocation functions"), que no deben ser confundidas con un asignador ("Allocator").

[3] Stroustrup TC++PL §19.4