koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

多次元の std::array を楽に扱う

はじめに

前回の記事では,多次元の std::vector について書いた. 今回は多次元の std::array について書こうと思う.

まず,std::array は組み込み配列と同等の機能を提供するクラスである(というより,組み込み配列のラッパークラスである). 使用方法としては std::array<int, 4> arr のように,第1テンプレート引数に要素型,第2テンプレート引数に要素数を渡す. std::array<int, 4> arrint arr[4] に相当する宣言となる.

しかし,std::array を多次元にする場合を考えると,

std::array<std::array<std::array<int, 30>, 20>, 10> arr;

と宣言が長い宣言が必要になる. また,上記の3次元の std::array と同等の組み込み配列は int arr[10][20][30] であり,要素数の順が逆になっている.

この記事では,これらの問題を解決することについて書く.

実装

以下のコードがこの記事に書きたいことの全てである.

#include <algorithm>
#include <array>
#include <iostream>
#include <iterator>
#include <type_traits>
#include <utility>


struct is_range_impl
{
  template<typename T>
  static auto
  check(T&& obj) -> decltype(std::begin(obj), std::end(obj), std::true_type{});

  template<typename T>
  static auto
  check(...) -> std::false_type;
};  // struct is_range_impl

template<typename T>
class is_range :
  public decltype(is_range_impl::check<T>(std::declval<T>()))
{};  // class is_range


template<
  typename R,
  typename T,
  typename std::enable_if<
    is_range<R>::value && !is_range<typename std::iterator_traits<decltype(std::begin(std::declval<R>()))>::value_type>::value,
    std::nullptr_t
  >::type = nullptr
>
static inline void
fill(R&& range, T&& value) noexcept
{
  std::fill(std::begin(range), std::end(range), std::forward<T>(value));
}


template<
  typename R,
  typename T,
  typename std::enable_if<
    is_range<R>::value && is_range<typename std::iterator_traits<decltype(std::begin(std::declval<R>()))>::value_type>::value,
    std::nullptr_t
  >::type = nullptr
>
static inline void
fill(R&& range, T&& value) noexcept
{
  for (auto&& e : range) {
    fill(std::forward<decltype(e)>(e), std::forward<T>(value));
  }
}


template<
  typename T,
  std::size_t kN,
  std::size_t... kSizes
>
struct ndarray_impl
{
  using type = std::array<typename ndarray_impl<T, kSizes...>::type, kN>;
};  // struct ndarray_impl

template<
  typename T,
  std::size_t kN
>
struct ndarray_impl<T, kN>
{
  using type = std::array<T, kN>;
};  // struct ndarray_impl

template<typename T, std::size_t kN, std::size_t... kSizes>
using NdArray = typename ndarray_impl<T, kN, kSizes...>::type;


int
main()
{
  NdArray<int, 4> arr1;
  fill(arr1, 0);
  for (const auto& e : arr1) {
    std::cout << e << " ";
  }
  std::cout << "\n\n";

  NdArray<int, 4, 4> arr2;
  fill(arr2, -1);
  for (const auto& e1 : arr2) {
    for (const auto& e2 : e1) {
      std::cout << e2 << " ";
    }
    std::cout << "\n";
  }
  std::cout << "\n";

  NdArray<int, 4, 4, 4> arr3;
  fill(arr3, 114514);
  for (const auto& e1 : arr3) {
    for (const auto& e2 : e1) {
      for (const auto& e3 : e2) {
        std::cout << e3 << " ";
      }
      std::cout << "\n";
    }
    std::cout << "\n";
  }
  std::cout << "\n";

  return 0;
}

std::array はテンプレート引数に要素数が必要なので,前回の記事NdVector とは異なり, NdArray<int, 10, 20, 30> とする必要がある. NdVector<int, 10, 20, 30> arr;int arr[10][20][30]; と同等であり,組み込み配列と遜色無い使用感になっていると思う.

なお,組み込み配列と同様に std::array 生成時に要素は初期化されないので,ローカル変数で使う分には初期化が必要となる. std::array は組み込み配列のみをメンバー変数に持つラッパークラスであり,全次元を通して領域が連続していることを利用して,

NdAarray<int, 3, 4, 5> arr;
std::fill(
  reinterpret_cast<int*>(arr.data()),
  reinterpret_cast<int*>(arr.data() + arr.size()),
  0);

のようにしてしまうのもアリだが,お行儀が悪いので,ちゃんとRange-based forでループを行って初期値を入れる関数 fill() を用意した.

なお,この fill() は他の多次元のものに対しても利用可能であるので,例えば多次元の組み込み配列にも適用可能である.

int arr[3][4][5][6];
fill(arr, 0);

当然,前回の記事の NdVector にも適用可能である. (各関数,クラスの実装は前回の記事を参照)

auto nv = makeNdVector(3, 4, 5, -1);

std::cout << "nv =\n[";
for (const auto& e1 : arr3) {
  std::cout << "  [\n";
  for (const auto& e2 : e1) {
    std::cout << "    [";
    for (const auto& e3 : e2) {
      std::cout << e3 << ", ";
    }
    std::cout << "],\n";
  }
  std::cout << "  ],\n";
}
std::cout << "]\n";


fill(nv, 0);

std::cout << "nv =\n[";
for (const auto& e1 : arr3) {
  std::cout << "  [\n";
  for (const auto& e2 : e1) {
    std::cout << "    [";
    for (const auto& e3 : e2) {
      std::cout << e3 << ", ";
    }
    std::cout << "],\n";
  }
  std::cout << "  ],\n";
}
std::cout << "]\n";

他にも多次元の std::list にも fill() を適用することが可能である.

別実装

いなむ神に以下のような実装を教えていただいた.

#include <array>


template<typename T, std::size_t N, std::size_t... Extents>
struct extents_expander
  : extents_expander<std::array<T, N>, Extents...>
{};  // struct extents_expander

template<typename T, std::size_t N>
struct extents_expander<T, N>
{
  using type = std::array<T, N>;
};  // struct extents_expander

template<typename T, std::size_t... Extents>
struct ndarray_helper
{
  using type = typename extents_expander<T, Extents...>::type;
};  // struct ndarray_helper

template<typename T, std::size_t N, std::size_t... Extents>
struct ndarray_helper<T[N], Extents...>
{
  using type = typename ndarray_helper<T, N, Extents...>::type;
};  // struct ndarray_helper

template<typename T>
using NdArray = typename ndarray_helper<T>::type;

これは以下のように使用できる.

NdArray<int[10][20][30]> arr;

こちらの方が直感的であると感じた.

まとめ

多次元 std::array の型の記述を簡単にするエイリアステンプレートの実装と,初期化関数を紹介した. 工夫すれば,多次元の std::array は多次元の組み込み配列と同じぐらい容易に扱うことが可能になる.