Artículos de la serie:
- Introducción
- Una plantilla base para el contenedor Dynarray<>
- Primeros constructores para la plantilla Dynarray<>
- Técnica copy-and-swap
- Semántica de movimiento
- Últimos pasos. Sentencias de control for basadas en rango
Últimos pasos. Sentencias de control for basadas en rango
En este último post de la serie, mostraremos el código completo de la plantilla Dynarray<> (que debería incluirse en un espacio de nombres conveniente), introduciendo funciones miembro que permitan iterar a través del array, acceder a sus elementos (con y sin verificación de rango), etcétera.La mayor parte del código es autoexplicativo y descansa en las discusiones realizadas en artículos anteriores:
namespace detail {
// incluir aquí la definición de Dynarray_base<>
}
template<typename T>
class Dynarray : protected detail::Dynarray_base<T> {
using Base = detail::Dynarray_base<T>;
using Base::first;
using Base::count;
public:
// typedefs:
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;
// construct/copy/move/destroy:
explicit Dynarray(size_type n = 0)
: Base{n}
{
auto current = first;
try {
for (; n > 0; ++current, --n)
::new(static_cast<void*>(current)) value_type;
}
catch (...) {
for (auto p = first; p != current; ++p)
p->~value_type();
throw;
}
}
Dynarray(size_type n, value_type const& val)
: Base{n}
{
std::uninitialized_fill_n(first,n,val);
}
Dynarray(std::initializer_list<value_type> const& init)
: Base{init.size()}
{
std::uninitialized_copy(init.begin(),init.end(),first);
}
Dynarray(Dynarray const& d)
: Base{d.size()}
{
std::uninitialized_copy(d.begin(),d.end(),first);
}
auto& operator=(Dynarray const& d)
{
Dynarray<value_type> tmp{d};
swap(tmp);
return *this;
}
Dynarray(Dynarray&& d) noexcept
: Base{0}
{
swap(d);
}
auto& operator=(Dynarray&& d) noexcept
{
Dynarray<value_type> tmp;
swap(tmp);
swap(d);
return *this;
}
~Dynarray()
{
for (auto p = begin(); p != end(); ++p)
p->~value_type();
}
// iterators:
auto begin() const noexcept { return const_iterator{first}; }
auto begin() noexcept { return first; }
auto end() const noexcept { return const_iterator{first + count}; }
auto end() noexcept { return first + count; }
auto cbegin() const noexcept { return const_iterator{first}; }
auto cend() const noexcept { return const_iterator{first + count}; }
auto rbegin() const noexcept { return const_reverse_iterator{cend()}; }
auto rbegin() noexcept { return reverse_iterator{end()}; }
auto rend() const noexcept { return const_reverse_iterator{first}; }
auto rend() noexcept { return reverse_iterator{first}; }
auto crbegin() const noexcept { return const_reverse_iterator{cend()}; }
auto crend() const noexcept { return const_reverse_iterator{first}; }
// capacity:
auto size() const noexcept -> size_type { return count; }
[[nodiscard]] auto empty() const noexcept -> bool { return count == 0; }
// element access:
auto operator[](size_type i) const -> const_reference { return first[i]; }
auto operator[](size_type i) -> reference { return first[i]; }
auto at(size_type i) const -> const_reference { rangeCheck(i); return first[i]; }
auto at(size_type i) -> reference { rangeCheck(i); return first[i]; }
// data access:
auto data() const noexcept -> T const* { return first; }
auto data() noexcept -> T* { return first; }
// modifiers:
void swap(Dynarray& d) noexcept
{
std::swap(first,d.first);
std::swap(count,d.count);
}
private:
void rangeCheck(size_type i) const
{
if (i >= size)
throw std::out_of_range{"Out of range index"};
}
};
En particular, las funciones miembro públicas begin() y end() permiten iterar a través de la matriz unidimensional en el sentido habitual, desde el primer elemento al último (es decir, según la secuencia de índices de acceso 0,1,2,...,n-1, siendo n el número total de entradas en el array). La función begin() retorna un iterador que apunta al primer elemento de la matriz, mientras que end() lo hace a un hipotético elemento de tipo value_type posterior al último (véase la figura superior). Las versiones constantes de dichas funciones, invocables por arrays constantes, retornan punteros a elementos constantes, de modo que el contenedor no pueda modificarse. Así operan también, por definición, cbegin() y cend(), si bien estas funciones pueden ser invocadas por objetos no constantes. Las funciones rbegin() y rend() (así como crbegin() y crend()) permiten iterar la matriz en orden inverso (desde el último elemento al primero, según la secuencia de acceso n-1,n-2,...,2,1,0). El siguiente bloque de código proporciona varios ejemplos de uso:
auto d = Dynarray<int>{0,1,2,3,4};
for (auto p = d.cbegin(); p != d.cend(); ++p)
std::cout << *p << ' '; // output: 0 1 2 3 4
for (auto p = d.begin(); p != d.end(); ++p)
*p *= 2; // multiplicamos cada entrada por 2
for (auto p = d.crbegin(); p != d.crend(); ++p)
std::cout << *p << ' '; // output: 8 6 4 2 0
Conviene resaltar aquí la sintaxis simplificada que los estándares del lenguaje C++ y posteriores ofrecen para recorrer por iteración cualquier rango definido por un par de funciones begin() y end(). Nos referimos a la denominada sentencia de control for basada en rango [1]. Así, los dos primeros bucles del código anterior podrían reescribirse como:
auto d = Dynarray<int>{0,1,2,3,4};
for (auto x : d)
std::cout << x << ' '; // output: 0 1 2 3 4
for (auto& x : d)
x *= 2; // multiplicamos cada entrada por 2
La sentencia for (auto x : d) en el primer bucle de ejemplo puede leerse como "para cada elemento x en el contenedor d". En este caso, se realiza una copia de cada elemento (se habla entonces de acceso por valor) para enviarla a la salida estándar. Si la copia de las entradas de la matriz fuese una operación costosa y, como en el caso considerado, no fuese necesario modificar sus valores, convendría tomar referencias a elementos constantes, escribiéndose en su lugar for (auto const& x : d) (acceso por referencia a objeto constante). En nuestro caso no es necesario proceder así, dado que trabajamos con enteros int: ¡una referencia puede ser más costosa que una copia!
En el segundo bucle de ejemplo, se toma una referencia no-constante de cada elemento de la matriz con el fin de poder modificar su valor, optándose por la forma for (auto& x : d) (acceso por referencia).
Referencias bibliográficas:
- Range-for loop - https://en.cppreference.com/w/cpp/language/range-for
- C++ Dynamic Arrays proposal (N3662) - https://isocpp.org/files/papers/N3662.html
No hay comentarios:
Publicar un comentario