Semánticas de copia y movimiento (III)

Artículos de la serie:

 Primeros constructores para la plantilla Dynarray<>


Tal y como se explicó en el anterior post de esta serie, la plantilla Dynarray<> se hace derivar de la plantilla base detail::Dynarray_base<> con el fin de simplificar el cuerpo de los constructores que definiremos para la clase, así como la gestión de la memoria en el free store:

   template<typename T>    class Dynarray : protected detail::Dynarray_base<T> {       using Base = detail::Dynarray_base<T>;       using Base::first;       using Base::count;    public:       // definiciones de tipos:       using value_type             = T;       using const_reference        = T const&;       using reference              = T&;       using const_iterator         = T const*;       using iterator               = T*;       using const_reverse_iterator = std::reverse_iterator<const_iterator>;       using reverse_iterator       = std::reverse_iterator<iterator>;       using size_type              = std::size_t;       // resto de la interfaz pública...    };

Observemos la introducción de múltiples declaraciones using en la interfaz pública de la clase. Así, por ejemplo, value_type es declarado como alias del tipo de elemento T almacenado en el array, const_reference es sinónimo de una referencia a un objeto constante de tipo T, etcétera. Nuestro objetivo al proceder así es tripe. En primer lugar, proporcionamos un listado coherente de tipos (compatible con la biblioteca estándar del lenguaje) que el programador puede utilizar para declarar variables en su propio código. Dichos tipos sirven, asimismo, de canal de comunicación natural con algoritmos. Por último, mejoramos la sintaxis de la interfaz pública, la cual, como comprobaremos más adelante, se torna más precisa.

Las declaraciones using poseen la misma semántica que aquéllas que utilizan el especificador typedef, muy común en código previo al estándar C++11. Así, using value_type = T es equivalente a typedef T value_type (véase, por ejemplo, [2]).

Llegados a este punto, procedemos a introducir varios constructores con los que poder inicializar el contenedor:

   template<typename T>    class Dynarray : protected detail::Dynarray_base<T> {       // ...    public:       // definiciones de tipos...       // construcción:       explicit Dynarray(size_type n = 0);                      // (A)        Dynarray(size_type n, value_type const& val);            // (B)       Dynarray(std::initializer_list<value_type> const& init); // (C)       // destrucción:       ~Dynarray();              // ...    };


Constructor público (A): Construye un array con n elementos de tipo value_type inicializados por defecto en el sentido dado por el estándar del lenguaje (consúltese el término default-initialization [3], que no debe confundirse con zero-initialization [4]). En particular, si value_type es un tipo primitivo, no se asignan valores a los elementos (éstos quedan indeterminados), mientras que si value_type es una clase, se invoca al constructor por defecto value_type::value_type() de la misma. Asumimos aquí, pues, que value_type es DefaultConstructible.

   template<typename T> inline    Dynarray<T>::Dynarray(size_type n)       : Base{n} // alojamos n*sizeof(T) bytes sin inicializar    {       auto current = first;       try {          for (; n > 0; ++current, --n)             ::new(static_cast<void*>(current)) value_type; // default-initialization       }       catch (...) {   // manejamos cualquier excepción          for (auto p = first; p != current; ++p)             p->~value_type();          throw;       // relanzamos la excepción       }    }

El constructor invoca al constructor de la clase base Dynarray_base<T> con el fin de asignar un espacio en memoria suficiente para almacenar los n elementos (véase la lista de iniciadores). De tener éxito tal operación, se procede a inicializar por defecto los n primeros elementos del área de memoria referenciada por first. El constructor ofrece la garantía fuerte ante excepciones, es decir, de emitirse una excepción, la función no tiene efecto: los objetos construidos satisfactoriamente antes de producirse la excepción son destruidos en orden inverso a su creación antes de relanzarse la excepción. En tal caso, un probable proceso de desenredo de la pila invocaría al destructor de la clase base, que se encarga de desalojar la memoria pre-asignada por su constructor.


Constructor público (B): Construye un array con n copias del valor val haciendo uso de la función estándar std::uninitialized_fill_n() [5], que ofrece la garantía fuerte ante excepciones. Asumimos aquí, pues, que value_type es CopyConstructible:

   template<typename T> inline    Dynarray<T>::Dynarray(size_type n, value_type const& val)       : Base{n}    {       std::uninitialized_fill_n(first, n, val); // los objetos se construyen in situ    }

El constructor anterior es funcionalmente equivalente a:

   template<typename T> inline    Dynarray<T>::Dynarray(size_type n, value_type const& val)       : Base{n}    {       auto current = first;       try {          for (; n > 0; ++current, --n)             ::new(static_cast<void*>(current)) value_type(val);       }       catch (...) {          for (auto p = first; p != current; ++p)             p->~value_type();          throw;       }    }


Constructor público (C): Construye un array a partir del contenido de una lista de inicialización de tipo std::initializer_list<value_type> [6]. Ello permite inicializar el array con una serie de valores especificados en el mismo lugar de su construcción. Por ejemplo, auto d = Dynarray<int>{0, 1, 2, 3}. El constructor de la clase base asigna espacio suficiente en memoria para almacenar tantos elementos como tenga la lista de inicialización, para a continuación copiar su contenido en dicho bloque de memoria haciendo uso de la función estándar std::uninitialized_copy() [7]. Como en los casos anteriores, el constructor ofrece la garantía fuerte ante excepciones:

   template<typename T> inline    Dynarray<T>::Dynarray(std::initializer_list<value_type> const& init)       : Base{init.size()} // alojamos init.size()*sizeof(T) bytes en el free store    {       // copiamos los elementos en el rango [init.begin(), init.end()) al       // área de memoria sin inicializar referenciada por this->first:       std::uninitialized_copy(init.begin(), init.end(), first);    }

Finalizaremos este post con la definición del destructor de la clase (implícitamente noexcept), encargado de destruir los elementos en la matriz y liberar la memoria asignada por el constructor de la clase base:

   template<typename T> inline    Dynarray<T>::~Dynarray()   {       auto const last = first + count;       for (auto p = first; p != last; ++p)          p->~value_type();    } // Base::~Base() desaloja la memoria apuntada por first al cierre del destructor


Referencias bibliográficas:
  1. std::is_nothrow_destructible<> - https://en.cppreference.com/w/cpp/types/is_destructible
  2. Alias declaration - https://stackoverflow.com/questions/10747810/what-is-the-difference-between-typedef-and-using-in-c11
  3. Default initialization - https://en.cppreference.com/w/cpp/language/default_initialization
  4. Zero initialization - https://en.cppreference.com/w/cpp/language/zero_initialization
  5. std::uninitialized_fill_n() - https://en.cppreference.com/w/cpp/memory/uninitialized_fill_n
  6. std::initializer_list<> - https://en.cppreference.com/w/cpp/utility/initializer_list
  7. std::uninitialized_copy() - https://en.cppreference.com/w/cpp/memory/uninitialized_copy

No hay comentarios:

Publicar un comentario