使用std::array声明二维(甚至更高维)数组的便捷方法

huangapple go评论116阅读模式
英文:

Convenient way to declare 2D (or even higher dimension) arrays with std::array

问题

我即将将大量旧的C++代码转换为更现代的C++代码。

在那些代码中有许多原始的二维数组,例如:

Foo bar[XSIZE][YSIZE];

我打算用以下方式替换这些声明:

std::array<std::array<Foo, YSIZE>, XSIZE> bar;

这是一种方便的方式,因为语句保持不变,代码应该与使用原始数组时的行为相同,并且额外的好处是在调试构建中可以进行越界检查。

但是我认为std::array<std::array<Foo, YSIZE>>有些繁琐且不易阅读,而且对于三维数组(虽然我没有)情况会更糟糕。

目前我正在使用以下宏来使声明更易读:

#define DECLARE_2D_ARRAY(type, x, y) std::array<std::array<type, y>, x>
...
DECLARE_2D_ARRAY(Foo, XSIZE, YSIZE) bar;

但我觉得这是一种宏的hack,我想知道是否有更清晰、更符合C++风格的方法来做类似的事情。

英文:

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&lt;std::array&lt;Foo, YSIZE&gt;, XSIZE&gt; 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&lt;std::array&lt;Foo, YSIZE&gt;&gt; is somewhat cumbersome and not easy to read, and with 3D arrays (although I have none) it would even be worse.

Right now I'm using this macro to make the declaration more readable:

#define DECLARE_2D_ARRAY(type, x, y) std::array&lt;std::array&lt;type, y&gt;, x&gt;
...
DECLARE_2D_ARRAY(Foo, XSIZE, YSIZE) bar;

But I feel this to be a macro hack, and I'm wondering if there is a cleaner, more C++ way to do something similar.

答案1

得分: 75

你可以使用类型别名模板来实现:

#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;
}

你也可以将其推广到任意维度:

#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; 
}
英文:

You can use a type alias template:

#include &lt;array&gt; 
#include &lt;cstddef&gt;

template &lt;class T, std::size_t x, std::size_t y&gt;
using Array2D = std::array&lt;std::array&lt;T, y&gt;, x&gt;;

int main() {
    Array2D&lt;int, 5, 3&gt; arr;
}

You can also generalize it like so for any dimension:

#include &lt;array&gt;
#include &lt;cstddef&gt;

template &lt;class T, std::size_t size, std::size_t... sizes&gt;
struct ArrayHelper {
    using type = std::array&lt;typename ArrayHelper&lt;T, sizes...&gt;::type, size&gt;;
};

template &lt;class T, std::size_t size&gt;
struct ArrayHelper&lt;T, size&gt; {
    using type = std::array&lt;T, size&gt;;
};

template &lt;class T, std::size_t... sizes&gt;
using Array = typename ArrayHelper&lt;T, sizes...&gt;::type;

int main() { 
    Array&lt;int, 5, 3, 4, 3&gt; arr; 
}

答案2

得分: 24

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>;
};

// 现在
array_t<Foo[XSIZE][YSIZE]>

std::array<std::array<Foo, XSIZE>, YSIZE>
的替代方案是:

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>;
};

如果你喜欢,可以使用以下语法:

array_t<Foo, XSIZE, YSIZE>

我们甚至可以结合两者,允许任一语法。

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>
{};

现在

array_t<Foo[XSIZE], YSIZE>
可以工作了。

但要小心 - 顺序很棘手!

int[3][2] 是一个包含 3 个元素的数组,每个元素又是一个包含 2 个元素的数组。

为了保持一致,我们希望

array_t<int, 3, 2>


std::array<std::array<int, 2>, 3>
而不是

std::array<std::array<int, 3>, 2>

以下是用于确定顺序是否正确的测试用例:

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]>>);

根据你选择的 `array_t`,删除语法错误的部分。

[在线示例](https://godbolt.org/z/f4Gz1cvGh)

现在,即使这样可能是错误的。感觉不正确的是

array_t<int[3], 2>
没有大小为 3 的子数组,然而

array_t<int[3][2]>
感觉应该与 `int[3][2]` 相同,并且 `int[3][2]` 的布局应与 `array_t<int[3][2]>`  `array_t<int, 3, 2>` 一致。

此外,`array_t<array_t<int, 3>, 2>` 应该与 `array_t<int[3], 2>` 相同。

这些要求彼此之间是不一致的。我的意思是,它们在各个地方都不一致。

可能解决这个问题最简单的方法是只允许 `[][][]` 语法,或者不允许混合使用 `[]`  `,` 语法。

 `array_t<int[3][2]>`  `int[3][2]` 具有相同的布局是很有价值的。同样,具有 `array_t<int, 3, 2>` 语法也很有价值。可能我们希望 `array_t<int, 3, 2>` 的含义与 `int[3][2]` 相同?放弃它与 `array_t<array_t<int, 3>, 2>` 相等的特性 - 取而代之,它等于 `array_t<array_t<int,2>,3>`。最后,阻止 `array_t<int[3], 2>` 语法,因为它容易引起混淆。

然后,将 `array_t<T, 1,2,3,...>`  `array_t<T[1][2][3]...>` 模板分开,以最小化混淆。
英文:
template&lt;class A&gt;
struct std_array_helper {
using type=A;
};
template&lt;class A&gt;
using array_t = typename std_array_helper&lt;A&gt;::type;
template&lt;class T, std::size_t N0&gt;
struct std_array_helper&lt;T[N0]&gt; {
using type=std::array&lt;array_t&lt;T&gt;, N0&gt;;
};

now

array_t&lt;Foo[XSIZE][YSIZE]&gt;

is

std::array&lt; std::array&lt;Foo, XSIZE&gt;, YSIZE&gt;

an alternative solutions is:

template&lt;class T, std::size_t...Sz&gt;
struct array_helper {
using type=T;
};
template&lt;class T0, std::size_t...Ns&gt;
using array_t = typename array_helper&lt;T0, Ns...&gt;::type;
template&lt;class T, std::size_t N0, std::size_t...Ns&gt;
struct array_helper&lt;T, N0, Ns...&gt;
{
using type=std::array&lt;array_t&lt;T, Ns...&gt;, N0&gt;;
};

this uses the syntax:

array_t&lt;Foo, XSIZE, YSIZE&gt;

if you prefer it.

We can even combine the two, allowing either syntax.

template&lt;class T, std::size_t...Sz&gt;
struct array_helper {
using type=T;
};
template&lt;class T0, std::size_t...Ns&gt;
using array_t = typename array_helper&lt;T0, Ns...&gt;::type;
template&lt;class T, std::size_t N0, std::size_t...Ns&gt;
requires (!std::is_array_v&lt;T&gt;)
struct array_helper&lt;T, N0, Ns...&gt;
{
using type = std::array&lt;array_t&lt;T, Ns...&gt;, N0&gt;;
};
template&lt;class T, std::size_t N0, std::size_t...Ns&gt;
struct array_helper&lt;T[N0], Ns...&gt;:
array_helper&lt;array_t&lt;T, Ns...&gt;, N0&gt;
{};

and now

array_t&lt; Foo[XSIZE], YSIZE &gt;

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&lt;int, 3, 2&gt;

to be

std::array&lt; std::array&lt; int, 2 &gt;, 3&gt;

not

std::array&lt; std::array&lt; int, 3 &gt;, 2&gt;

here are test cases to determine if you got the order right:

static_assert( std::is_same_v&lt; std::array&lt;int, 3&gt;, array_t&lt;int, 3&gt; &gt; );
static_assert( std::is_same_v&lt; std::array&lt; std::array&lt;int, 2&gt;, 3&gt;, array_t&lt;int, 3, 2&gt; &gt; );
static_assert( std::is_same_v&lt; std::array&lt; std::array&lt;int, 2&gt;, 3&gt;, array_t&lt;int[3], 2&gt; &gt; );
static_assert( std::is_same_v&lt; std::array&lt; std::array&lt;int, 2&gt;, 3&gt;, array_t&lt;int[3][2]&gt; &gt; );

remove whichever have the wrong syntax for your chosen array_t.

Live example

Now, even this might be wrong. It feels incorrect that

array_t&lt;int[3], 2&gt;

doesn't have sub-arrays of size 3, yet

array_t&lt;int[3][2]&gt;

feels like it should also be the same array, and the layout of int[3][2] should agree with array_t&lt;int[3][2]&gt; and agree with array_t&lt;int, 3, 2&gt;.

Also, array_t&lt; array_t&lt;int, 3&gt;, 2&gt; should be the same as array_t&lt;int[3], 2&gt;.

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&lt;int[3][2]&gt; with the same layout as int[3][2] is high value. Similarly, having array_t&lt; int, 3, 2 &gt; syntax is high value. Probably we want array_t&lt;int, 3, 2&gt; to mean the same as int[3][2]? Throw away this being equal to array_t&lt; array_t&lt;int, 3&gt;, 2&gt; - instead it equals array_t&lt;array_t&lt;int,2&gt;,3&gt;. Finally, block array_t&lt;int[3], 2&gt; syntax as confusing.

Then, split the array_t&lt; T, 1,2,3,...&gt; from array_t&lt;T[1][2][3]...&gt; templates to minimize confusion.

答案3

得分: 8

在C++23中,你可以使用std::mdspan来获取单维数组的多维视图。有一个关于std::mdarray提案,但它至少要到C++26才会出现在C++中。

下面是使用std::mdspan的一个示例:

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';
}       
英文:

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&lt;Foo, XSIZE * YSIZE&gt; 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 &lt;&lt; bar[x, y] &lt;&lt; &#39; &#39;;
}
std::cout &lt;&lt; &#39;\n&#39;;
}       

huangapple
  • 本文由 发表于 2023年8月8日 21:40:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/76860140.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定