17. Creating a 2D array with basic operations

Before looking at how we could define such a structure, let's consider several test cases for it. The following snippet shows all the functionality that was requested:

int main()
{
// element access
array2d<int, 2, 3> a {1, 2, 3, 4, 5, 6};
for (size_t i = 0; i < a.size(1); ++i)
for (size_t j = 0; j < a.size(2); ++j)
a(i, j) *= 2;

// iterating
std::copy(std::begin(a), std::end(a),
std::ostream_iterator<int>(std::cout, " "));

// filling
array2d<int, 2, 3> b;
b.fill(1);

// swapping
a.swap(b);

// moving
array2d<int, 2, 3> c(std::move(b));
}

Note that for element access, we are using operator(), such as in a(i,j), and not operator[], such as in a[i][j], because only the former can take multiple arguments (one for the index on each dimension). The latter can only have a single argument, and in order to enable expressions like a[i][j], it has to return an intermediate type (one that basically represents a row) that in turn overloads operator[] to return a single element. 

There are already standard containers that store either fixed or variable-length sequences of elements. This two-dimensional array class should be just an adapter for such a container. In choosing between std::array and std::vector, we should consider two things:

  • The array2d class should have move semantics to be able to move objects
  • It should be possible to list initialize an object of this type

The std::array container is movable only if the elements it holds are move-constructible and move-assignable. On the other hand, it cannot be constructed from an std::initializer_list. Therefore, the more viable option remains an std::vector.

Internally, this adapter container can store its data either in a vector of vectors (each row is a vector<T> with C elements, and the 2D array has R such elements stored in a vector<vector<T>>) or single vector of RC elements of type T. In the latter case, the element on row i and column j is found at index i * C + j. This approach has a smaller memory footprint, stores all data in a single contiguous chunk, and is also simpler to implement. For these reasons, it is the preferred solution.

A possible implementation of the two-dimensional array class with the requested functionality is shown here:

template <class T, size_t R, size_t C>
class array2d
{
typedef T value_type;
typedef value_type* iterator;
typedef value_type const* const_iterator;
std::vector<T> arr;
public:
array2d() : arr(R*C) {}
explicit array2d(std::initializer_list<T> l):arr(l) {}
constexpr T* data() noexcept { return arr.data(); }
constexpr T const * data() const noexcept { return arr.data(); }

constexpr T& at(size_t const r, size_t const c)
{
return arr.at(r*C + c);
}

constexpr T const & at(size_t const r, size_t const c) const
{
return arr.at(r*C + c);
}

constexpr T& operator() (size_t const r, size_t const c)
{
return arr[r*C + c];
}

constexpr T const & operator() (size_t const r, size_t const c) const
{
return arr[r*C + c];
}

constexpr bool empty() const noexcept { return R == 0 || C == 0; }

constexpr size_t size(int const rank) const
{
if (rank == 1) return R;
else if (rank == 2) return C;
throw std::out_of_range("Rank is out of range!");
}

void fill(T const & value)
{
std::fill(std::begin(arr), std::end(arr), value);
}

void swap(array2d & other) noexcept { arr.swap(other.arr); }

const_iterator begin() const { return arr.data(); }
const_iterator end() const { return arr.data() + arr.size(); }
iterator begin() { return arr.data(); }
iterator end() { return arr.data() + arr.size(); }
};
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset