# 2D counterpart of std::array in C++17

I implemented a 2D counterpart of std::array named array2d in C++17. It is an aggregate like std::array, and provides similar interface. The goal is that if you know how to use std::array, then you will find yourself at home using array2d. Any comments are welcome :) For better viewing experience with highlighting, you can refer to this GitHub page.

#include <cstddef>
#include <array>
#include <iterator>

template <typename T, std::size_t N0, std::size_t N1>
struct array2d {
using row_t = std::array<T, N1>;
inline static constexpr std::array sizes{ N0, N1 };

static constexpr std::size_t size() noexcept { return N0 * N1; }
static constexpr bool empty() noexcept { return !size(); }

T& at(std::size_t i, std::size_t j) { return data_.at(i).at(j); }
const T& at(std::size_t i, std::size_t j) const { return data_.at(i).at(j); }

row_t& operator[](std::size_t i) noexcept { return data_[i]; }
const row_t& operator[](std::size_t i) const noexcept { return data_[i]; }

T& front() { return data_.front().front(); }
const T& front() const { return data_.front().front(); }

T& back() { return data_.back().back(); }
const T& back() const { return data_.back().back(); }

T* data() noexcept { return data_.data()->data(); }
const T* data() const noexcept { return data_.data()->data(); }

T* begin() noexcept { return data(); }
const T* begin() const noexcept { return data(); }

T* end() noexcept { return data() + size(); }
const T* end() const noexcept { return data() + size(); }

auto rbegin() noexcept { return std::make_reverse_iterator(end()); }
auto rbegin() const noexcept { return std::make_reverse_iterator(end()); }

auto rend() noexcept { return std::make_reverse_iterator(begin()); }
auto rend() const noexcept { return std::make_reverse_iterator(begin()); }

void fill(const T& v) {
for (auto& row : data_) {
row.fill(v);
}
}

friend void swap(array2d& a, array2d& b) { a.data_.swap(b.data_); }

std::array<row_t, N0> data_;
};


Let me collect a couple of thoughts here.

• Aggregate initialization currently works like this:

array2d<int, 2, 2> a{{1, 2, 3, 4}};


but wouldn't it be favorable to allow for

array2d<int, 2, 2> a{{1, 2}, {3, 4}};

• std::array::at performs bound checking and throws upon an out of bounds index. When your intention is to stick with the std::array interface, you should do the same.

• If you want the container to be standard-compliant, there are some type aliases missing and maybe more. In particular, there are no cbegin(), cend(), crbegin(), crend() member functions. Is this intended?

• You implicitly use row-major order. Are you sure everyone expects this? Users familiar with Eigen and their fixed size matrices might at least want to customize row-/column-major ordering, e.g. Eigen::Matrix<int, 2, 2, Eigen::ColMajor> m;

• A range based for loop will considerably differ from a manual loop over rows and columns. Example:

// Loop over elements, transposed access. Requires nested loop.
for (std::size_t i = 0; i < 2; ++i)
for (std::size_t j = 0; j < 2; ++j)
std::cout << a[j][i] << "\n";

// Loop over elements, tranposed access impossible. Only one loop.
for (const auto& i : d)
std::cout << i << "\n";


This is slightly unintuitive. Shouldn't the range based for loop require a nested loop as well?

• The static data member sizes is not used anywhere.

Getting a two-dimensional array to work is not that much of an effort. Getting the semantics right is hard. Sticking to the std::array interface is a good goal when ease of use is intended for those familiar with the std::array template. But the additional dimension pulls in requirements that can't be tackled with the concepts of std::array. I would recommend having a look at established linear algebra libraries and their fixed size matrices. Also, the mdspan proposal for a multi-dimensional view on array types might be a good read.

• Thanks for reviewing my code. 1) You can a{1, 2, 3, 4} and make a new line as you see fit. 2) array2d::at does bounds checking too, for array::at is called under the hood ^_^ 3) You are right they are missing. I think they are tedious and not very useful in practice. But again, the array2d is not very useful either XD 4) It is by design that the layout aligns with built-in two-dimensional array. 5) I intend the loop to be similar to what it does with array. User needs to use other interface if different traversal is desired. 6) sizes is part of the interface. – Lingxi Mar 14 at 11:16
• array2d is not meant to model matrix in linear algebra. Just like array and vector are not meant to model vector in linear algebra. By design, it's only meant to serve the role of a basic fixed-size 2D container. Hopefully more convenient to use than built-in 2D array and nested array. – Lingxi Mar 14 at 11:16
• You're right with the bounds checking of course :) Also, the point that array2d is not meant to be used as a linear algebra vocabulary type is obviously valid. I do see two issues with that, though: if you introduce array2d to a code base, developers might use it for linear algebra despite the fact that you didn't design it to fit these requirements. And, I personally use two-dimensional arrays for linear algebra and nothing else. If I need more that one dimension, I often find other data structures nearer to my intention. – lubgr Mar 14 at 11:28

Looks good! Great job.

1. To initialize the array completely I have to write:

array2d<int, 2, 2> Array{{{{1, 2}, {2, 3}}}};


The two extra sets of braces are horrible! If you instead have a T[][] data member one layer of braces falls of and you need only one set of braces just like std::array.

array2d<int, 2, 2> Array{{{1, 2}, {2, 3}}}; // manageable

2. Whatever happened to constexpr all the things? :)

3. Nested std::arrays are not guaranteed to be continuous (see this post), although in practice they probably are. Resolving point 1) also fixes this issue.

4. IMO a at member that takes only one index and returns a row would make sense for consistency with your operator[].

5. Consider adding the various member types that a Container is supposed to have (and also the other requirements, cr[begin, end], max_size, member swap, ...).

6. I mean sure, size and empty can be static, but really, conceptually this doesn't make much sense. std::array's empty and size are not static too.

7. How about providing various customization points of std::get, std::tuple_size, ... so that your array works with structured bindings.

8. You didn't add any relational operators. Is this intentional?

• Thanks for reviewing my code, Rakete. 1) Since C++14, you can simply Array{1, 2, 4, 5} without any nested braces. 2) You are right they are missing. It's tedious and a burden, and I don't think they are very useful in practice. So I don't bother :/ 3) Good catch. Today I learned. 4) On a second thought, agreed. We can have at overloads that take 1 and 2 indices ^_^ 5) Yes. For serious production code, they should be present. The boring part of writing C++ library code XD – Lingxi Mar 15 at 1:56
• 6) User can still invoke them the way as if they are non-static like a.size(). Making them static provides more possibilities, and removes the need to pass this. 7) You are right. 8) Not until operator<=> is practically supported by the compilers :P – Lingxi Mar 15 at 2:01
• @Lingxi 1) How did I not know that, thanks :) 2) you really should, it's only one keyword ;) 6) yeah, but still thinks it's weird. 8) that would require that T also has an operator<=>. If it has the traditional operators, then you won't be able to compare the array since operator<=> can't dispatch to the individual relational operators of T. – Rakete1111 Mar 15 at 9:04