Semánticas de copia y movimiento (II)

Artículos de la serie:

Una plantilla base para el contenedor Dynarray<>


Nos proponemos implementar un contenedor Dynarray<> que encapsule arrays de tamaño fijo alojados en el free store:

   template<typename T> // T debe ser un tipo    class Dynarray;

Siguiendo la técnica RAII, un constructor de la clase será el encargado de asignar la memoria dinámica, mientras que el destructor la liberará al concluir la duración de almacenamiento del objeto.

Esta nueva serie de artículos tiene una motivación puramente educativa y algunas de las elecciones en la implementación del contenedor serán ciertamente discutibles y mejorables. Nuestro código se inspirará en las implementaciones de estructuras dinámicas de datos estándar facilitadas por compiladores como GCC [1], así como (vagamente) en anteriores propuestas de inclusión de un contenedor std::dynarray<> en el estándar del lenguaje [2].

Con el fin de simplificar la definición de los múltiples constructores con que dotaremos a la plantilla y la gestión de la memoria dinámica, y con la vista puesta en una futura extensión del código que permita la modificación del tamaño del array en tiempo de ejecución (al estilo del contenedor estándar std::vector<> [3]), conviene introducir una plantilla de clase base, Dynarray_base<>, de la que derivará Dynarray<>, tal y como se especifica en el código siguiente:

   namespace detail {       template<typename T>       struct Dynarray_base {          T* first;          // dirección de inicio del array          std::size_t count; // número de elementos          explicit Dynarray_base(std::size_t n)             : first{n? alloc(n) : nullptr}, count{n} { }          static auto alloc(std::size_t n) -> T*          {             if (n >= std::numeric_limits<std::size_t>::max()/sizeof(T))                throw std::bad_array_new_length{};             // nota: omitir la búsqueda de funciones de alojamiento específicas             // en el ámbito de la clase T             return static_cast<T*>(::operator new(n*sizeof(T)));          }          void dealloc() noexcept          {             // desalojo de la memoria apuntada por first             ::operator delete(static_cast<void*>(first));          }          ~Dynarray_base() { dealloc(); }       };    }


En función del valor del entero sin signo n pasado como argumento, el constructor inicializa el puntero first (que apunta al primer elemento del array) mediante una de las dos operaciones siguientes:
  • De ser n mayor que cero, el constructor llama a la función miembro estática alloc() para asignar un espacio en memoria suficiente para almacenar dicho número de elementos de tipo T. Para ello, se hace uso del operador global ::new. Observemos que la llamada a la función alloc() puede provocar la emisión de una excepción std::bad_alloc [4] de agotarse la memoria, o una excepción std::bad_array_new_length [5] si n supera el número máximo de elementos permitido por la implementación.
  • Si n es cero, first es inicializado como puntero nulo. En este último caso, el constructor no emitirá excepciones de ninguna clase.
Con el fin de desalojar manualmente la memoria asignada, se proporciona la función miembro dealloc(), que a su vez llama al operador global ::delete. El destructor de la plantilla base llama a esta función de forma automática.

La inicialización de los elementos de tipo T en el array es una tarea que corresponderá a los constructores de la clase Dynarray<T> y que analizaremos en el próximo artículo de la serie.

Hemos optado por implementar la plantilla base de manera que todos sus miembros sean públicos por defecto, en el bien entendido de que, al pertenecer ésta a un espacio de nombres llamado detail, ningún usuario procederá a generar instancias de la misma. La clase Dynarray<T> se hace entonces derivar de detail::Dynarray_base<T> de forma que todos los miembros públicos de la estructura base (sean éstos datos o funciones) pasen a ser protegidos en la clase derivada. Todo objeto de tipo Dynarray<T> posee, pues, un subobjeto detail::Dynarray_base<T> conteniendo las variables first y count. Tanto éstas como las funciones miembro detail::Dynarray_base<T>::alloc() y detail::Dynarray_base<T>::dealloc() podrán ser accedidas por la plantilla derivada precediendo sus nombres con la fórmula this->. Por ejemplo, la función miembro begin() definida más abajo retorna un puntero al primer elemento de la matriz:

   template<typename T>    class Dynarray : protected detail::Dynarray_base<T> {       // ...    public:       // ...       auto begin() noexcept -> T* { return this->first; }    };

En la función begin() anterior, una expresión de la forma return first, sin this-> precedente, provocaría un error de compilación (al menos en los compiladores 100% conformes con el estándar del lenguaje), pues el compilador no realiza una búsqueda de nombres en clases base dependientes del parámetro T (en nuestro caso, detail::Dynarray_base<T>) cuando intenta localizar la procedencia de nombres no-dependientes como first o count. Esta incidencia ocurre siempre que una plantilla derivada utiliza un miembro heredado de una plantilla base (véase el enlace [6] para más detalles).

Para exponer, al menos, los datos miembros de la plantilla base en la plantilla derivada y eliminar la necesidad de escribir repetidamente la fómula this->, simplificando así la definición de nuevas funciones miembro, podemos hacer uso de declaraciones using [7] tal y como se indica en el siguiente código:

   template<typename T>    class Dynarray : protected detail::Dynarray_base<T> {       using Base = detail::Dynarray_base<T>; // simple alias       // introducimos Base::first y Base::count en la definición de Dynarray:       using Base::first;       using Base::count;    public:       // ...       auto begin() noexcept -> T* { return first; }    };


Referencias bibliográficas:
  1. GCC Compilers - https://gcc.gnu.org/
  2. C++ Dynamic Arrays proposal (N3662) - https://isocpp.org/files/papers/N3662.html
  3. std::vector<> - https://en.cppreference.com/w/cpp/container/vector
  4. std::bad_alloc - https://en.cppreference.com/w/cpp/memory/new/bad_alloc
  5. std::bad_array_new_length - https://en.cppreference.com/w/cpp/memory/new/bad_array_new_length
  6. Nondependent name-lookup members - https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members
  7. using declarations - https://en.cppreference.com/w/cpp/language/using_declaration

No hay comentarios:

Publicar un comentario