26
I’m about to convert a lot of old C++ code to more modern C++.
There are many raw 2D arrays in that code like:
Foo bar[XSIZE][YSIZE];
And I’m about to replace these declarations with
std::array<std::array<Foo, YSIZE>, XSIZE> bar;
This is a convenient way because the statements stay the same and the code is supposed to behave the same as with raw arrays with the additional benefit of being able to have out of bounds checks in debug builds.
But IMO the std::array<std::array<Foo, YSIZE>>
is somewhat cumbersome and not easy to read, and with 3D arrays (although I have none) it would be even worse.
Right now I’m using this macro to make the declaration more readable:
#define DECLARE_2D_ARRAY(type, x, y) std::array<std::array<type, y>, x>
...
DECLARE_2D_ARRAY(Foo, XSIZE, YSIZE) bar;
But I feel this to be a macro hack, and I wonder if there is a cleaner, more C++ way to do something similar.
3 Answers
Reset to default
46
You can use a type alias template:
#include <array>
#include <cstddef>
template <class T, std::size_t x, std::size_t y>
using Array2D = std::array<std::array<T, y>, x>;
int main() {
Array2D<int, 5, 3> arr;
}
You can also generalize it like so for any dimension:
#include <array>
#include <cstddef>
template <class T, std::size_t size, std::size_t... sizes>
struct ArrayHelper {
using type = std::array<typename ArrayHelper<T, sizes...>::type, size>;
};
template <class T, std::size_t size>
struct ArrayHelper<T, size> {
using type = std::array<T, size>;
};
template <class T, std::size_t... sizes>
using Array = typename ArrayHelper<T, sizes...>::type;
int main() {
Array<int, 5, 3, 4, 3> arr;
}
17
template<class A>
struct std_array_helper {
using type=A;
};
template<class A>
using array_t = typename std_array_helper<A>::type;
template<class T, std::size_t N0>
struct std_array_helper<T[N0]> {
using type=std::array<array_t<T>, N0>;
};
now
array_t<Foo[XSIZE][YSIZE]>
is
std::array< std::array<Foo, XSIZE>, YSIZE>
an alternative solutions is:
template<class T, std::size_t...Sz>
struct array_helper {
using type=T;
};
template<class T0, std::size_t...Ns>
using array_t = typename array_helper<T0, Ns...>::type;
template<class T, std::size_t N0, std::size_t...Ns>
struct array_helper<T, N0, Ns...>
{
using type=std::array<array_t<T, Ns...>, N0>;
};
this uses the syntax:
array_t<Foo, XSIZE, YSIZE>
if you prefer it.
We can even combine the two, allowing either syntax.
template<class T, std::size_t...Sz>
struct array_helper {
using type=T;
};
template<class T0, std::size_t...Ns>
using array_t = typename array_helper<T0, Ns...>::type;
template<class T, std::size_t N0, std::size_t...Ns>
requires (!std::is_array_v<T>)
struct array_helper<T, N0, Ns...>
{
using type = std::array<array_t<T, Ns...>, N0>;
};
template<class T, std::size_t N0, std::size_t...Ns>
struct array_helper<T[N0], Ns...>:
array_helper<array_t<T, Ns...>, N0>
{};
and now
array_t< Foo[XSIZE], YSIZE >
works.
But be careful – the order is tricky!
int[3][2] is an array of 3 elements of arrays of 2 elements.
To keep this the same we want
array_t<int, 3, 2>
to be
std::array< std::array< int, 2 >, 3>
not
std::array< std::array< int, 3 >, 2>
here are test cases to determine if you got the order right:
static_assert( std::is_same_v< std::array<int, 3>, array_t<int, 3> > );
static_assert( std::is_same_v< std::array< std::array<int, 2>, 3>, array_t<int, 3, 2> > );
static_assert( std::is_same_v< std::array< std::array<int, 2>, 3>, array_t<int[3], 2> > );
static_assert( std::is_same_v< std::array< std::array<int, 2>, 3>, array_t<int[3][2]> > );
remove whichever have the wrong syntax for your chosen array_t
.
Now, even this might be wrong. It feels incorrect that
array_t<int[3], 2>
doesn’t have sub-arrays of size 3, yet
array_t<int[3][2]>
feels like it should also be the same array, and the layout of int[3][2]
should agree with array_t<int[3][2]>
and agree with array_t<int, 3, 2>
.
Also, array_t< array_t<int, 3>, 2>
should be the same as array_t<int[3], 2>
.
These requirements disagree with each other. I mean, all over the place they disagree.
Probably the simplest way to resolve this is to require only [][][]
syntax, or don’t permit mixed []
and ,
syntax.
Having array_t<int[3][2]>
with the same layout as int[3][2]
is high value. Similarly, having array_t< int, 3, 2 >
syntax is high value. Probably we want array_t<int, 3, 2>
to mean the same as int[3][2]
? Throw away this being equal to array_t< array_t<int, 3>, 2>
– instead it equals array_t<array_t<int,2>,3>
. Finally, block array_t<int[3], 2>
syntax as confusing.
Then, split the array_t< T, 1,2,3,...>
from array_t<T[1][2][3]...>
templates to minimize confusion.
5
-
2
I like this solution. It's pretty clever, especially as it works for any dimension.
– Jabberwockyyesterday
-
@Jabberwocky careful – my first pass got the dimension order wrong. Fixing now.
– Yakk – Adam Nevraumontyesterday
-
Just noticed it myself, thanks.
– Jabberwockyyesterday
-
@Jabberwocky Didn't catch it until I added "unit tests". 😉
– Yakk – Adam Nevraumontyesterday
-
Testing code. Not just for n00bs.
– user4581301yesterday
2
In C++23 you can use std::mdspan
to get a multi-dimensional view of a single-dimensional array. There is a proposal for a std::mdarray
, but that will not be in C++ until at least C++26.
An example of how std::mdspan
can be used:
std::array<Foo, XSIZE * YSIZE> bar_1d;
std::mdspan bar(bar_1d.data(), XSIZE, YSIZE);
…
for (std::size_t y = 0; y != YSIZE; ++y) {
for (std::size_t x = 0; x != XSIZE; ++x) {
std::cout << bar[x, y] << ' ';
}
std::cout << 'n';
}
1
-
1
Looks like no compiler has a full mdspan implementation yet.
– Aykhan Hagverdili21 hours ago
Not the answer you're looking for? Browse other questions tagged
or ask your own question.
or ask your own question.
|