koturnの日記

転職したい社会人2年生の技術系日記

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

はじめに

C++には std::vector という動的配列を扱うクラスが用意されている. このクラスはよく利用され,時には std::vectorstd::vector,すなわち多次元の std::vecotr が用いられることがある.

しかし,この多次元の std::vector

std::vector<std::vector<int>> vct2d;

のように長い型宣言を伴うのと,初期サイズを指定する場合,

std::vector<std::vector<int>> vct2d(5, std::vector<int>(10));

のような記述となり,なかなかに煩雑である.

この記事では,上記のような多次元 std::vector をある程度楽に扱えるようにすることについて記述する.

多次元の std::vector を生成する関数

まずは実装を紹介する.

#include <vector>
#include <type_traits>
#include <utility>


template<
  typename T,
  typename U
>
static inline std::vector<U>
makeNdVector(T&& n, U&& val) noexcept
{
  static_assert(std::is_integral<T>::value, "[makeNdVector] 1st argument must be an integer type");
  return std::vector<U>(std::forward<T>(n), std::forward<U>(val));
}


#if __cplusplus >= 201402L
template<
  typename T,
  typename... Args
>
static inline decltype(auto)
makeNdVector(T&& n, Args&&... args) noexcept
{
  static_assert(std::is_integral<T>::value, "[makeNdVector] 1st argument must be an integer type");
  return std::vector<decltype(makeNdVector(std::forward<Args>(args)...))>(std::forward<T>(n), makeNdVector(std::forward<Args>(args)...));
}
#else
template<
  typename T,
  typename... Args
>
static inline auto
makeNdVector(T&& n, Args&&... args) noexcept
  -> decltype(std::vector<decltype(makeNdVector(std::forward<Args>(args)...))>(std::forward<T>(n), makeNdVector(std::forward<Args>(args)...)))
{
  static_assert(std::is_integral<T>::value, "[makeNdVector] 1st argument must be an integer type");
  return std::vector<decltype(makeNdVector(std::forward<Args>(args)...))>(std::forward<T>(n), makeNdVector(std::forward<Args>(args)...));
}
#endif


#include <iostream>


int
main()
{
  // 10x5 の 全要素が 1 の 2次元vectorを生成
  auto vct2d = makeNdVector(5, 10, 1);
  for (std::size_t i = 0; i < 5; i++) {
    for (std::size_t j = 0; j < 10; j++) {
      std::cout << vct2d[i][j] << " ";
    }
    std::cout << "\n";
  }

  return 0;
}

C++11でもコンパイル可能なように,返り値の型を懇切丁寧に記述したものも用意した.

makeNdVector() 関数は,std::vector のコンストラクタと同様に扱えるように,一番最後の要素が全要素の初期値とする値にし,他は各次元における要素数とした. 要素数の順は多次元の組み込み配列と同じになるようにしている. 例えば,int arr[10][20][30] に対応するのは makeNdVector(10, 20, 30, 0); である.

多次元 std::vector の型の簡略化

多次元 std::vector を生成する関数の戻り値を auto で受け取る分には問題ないが,他の関数に渡すとなったときに void f(const std::vector<std::vector>& vct) と書かなくてはならない問題が残っている. そこで,型を簡略に記述できるようにしてみる.

#include <type_traits>
#include <vector>


template<
  typename T,
  std::size_t N,
  typename std::enable_if<(N > 0), std::nullptr_t>::type = nullptr
>
struct ndvector_impl
{
  using type = std::vector<typename ndvector_impl<T, N - 1>::type>;
};  // struct ndvector_impl


template<typename T>
struct ndvector_impl<T, 1>
{
  using type = std::vector<T>;
};  // struct ndvector_impl


template<
  typename T,
  std::size_t N
>
using NdVector = typename ndvector_impl<T, N>::type;


int
main()
{
  static_assert(std::is_same<NdVector<int, 1>, std::vector<int>>::value, "NG");
  static_assert(std::is_same<NdVector<double, 2>, std::vector<std::vector<double>>>::value, "NG");
  static_assert(std::is_same<NdVector<int, 4>, std::vector<std::vector<std::vector<std::vector<int>>>>>::value, "NG");

  return 0;
}

上記のコードに示す通り,例えば std::vector<std::vector<int>>NdVector<int, 2> と書けるようになる. これで関数の引数として型を記述するのも楽になる.

#include <vector>
#include <type_traits>
#include <utility>


template<
  typename T,
  typename U
>
static inline std::vector<U>
makeNdVector(T&& n, U&& val) noexcept
{
  static_assert(std::is_integral<T>::value, "[makeNdVector] The 1st argument must be an integer");
  return std::vector<U>(std::forward<T>(n), std::forward<U>(val));
}


template<
  typename T,
  typename... Args
>
static inline decltype(auto)
makeNdVector(T&& n, Args&&... args) noexcept
{
  static_assert(std::is_integral<T>::value, "[makeNdVector] The 1st argument must be an integer");
  return std::vector<decltype(makeNdVector(std::forward<Args>(args)...))>(std::forward<T>(n), makeNdVector(std::forward<Args>(args)...));
}


template<
  typename T,
  std::size_t N,
  typename std::enable_if<(N > 0), std::nullptr_t>::type = nullptr
>
struct ndvector_impl
{
  using type = std::vector<typename ndvector_impl<T, N - 1>::type>;
};  // struct ndvector_impl


template<typename T>
struct ndvector_impl<T, 1>
{
  using type = std::vector<T>;
};  // struct ndvector_impl


template<
  typename T,
  std::size_t N
>
using NdVector = typename ndvector_impl<T, N>::type;


#include <iostream>


static inline void
showAllElement(const NdVector<int, 2>& vct2d) noexcept
{
  for (const auto& row : vct2d) {
    for (const auto& col : row) {
      std::cout << col << " ";
    }
    std::cout << "\n";
  }
}


int
main()
{
  auto vct2d = makeNdVector(5, 10, 1);
  showAllElement(vct2d);

  return 0;
}

(どうでもいいけど,何故かメタ関数はスネークケース,インスタンス作ったりするクラスはパスカルケースで書きたい気持ちがあるんだよなぁ)

まとめ

多次元 std::vector を簡単に生成する関数と,型の記述を簡単にするエイリアステンプレートの実装を紹介した.

なお,この記事で紹介した方法では std::vector の第2テンプレート引数(メモリアロケータ)を指定できるようにはなっていないが,各次元において別のメモリアロケータを指定する場合などを考えると複雑であるし,そもそもメモリアロケータを指定したい人がわざわざ要素アクセス速度の遅い多次元の std::vector を使うとは思えないので省略した. (多次元 std::vector はいわゆるジャグ配列なので,全次元を通してメモリが連続に確保されるとはいえないので,キャッシュ効率の観点から不利なのは明らか)