C++
array

添字に受ける型を限定した配列の様な型を定義する

C++における暗黙的型変換というのは厄介である。例えば、複数のオブジェクトを管理するクラスがあったとする、そのクラスは要素であるオブジェクトをItemIDという型で識別する時、以下のような定義である。この時、ItemIDはユーザー定義型変換により数値型から暗黙的に変換できてしまう。

struct ItemID {
  ItemID(size_t id) : id_(id) {}
  operator size_t() const { return id_; }
private:
  size_t id_;
};

struct ObjectManager {
  auto operator [](ItemID id) { return objects_[id]; }
  class Object* objects_[100];
};

int main() {
  ObjectManager m;
  int i = 10;
  auto&& x = m[i]; // 添え字にintが使用できてしまう
}

多くの場合、この様な暗黙的型変換は、無意識のうちに配列のオーバーアクセスをしてしまうリスクがある。この様なリスクを回避する為に、使われるのがexplicitである。上記のItemIDのコンストラクタにexplicitを付けると、暗黙的型変換がエラーとなる。

struct ItemID {
  explicit ItemID(size_t id) : id_(id) {}
  operator size_t() const { return id_; }
private:
  size_t id_;
};

struct ObjectManager {
  auto operator [](ItemID id) { return objects_[id]; }
  class Object* objects_[100];
};

int main() {
  ObjectManager m;
  int i = 10;
  auto&& x = m[i]; // error
}

これで無意識のうちに不正な値を渡す事は無いだろう。識別子がクラスの場合はこれで良い。しかし、識別子は一つとは限らない。レガシーコードでよく見かける例で、enumと配列の組み合わせによる異なる識別子による配列アクセスがある。

enum DataID {
  A, B, C,
  NUM
};

enum DataName {
  HOGE, FUGA, PIYO
};

int data[NUM] = {0};

int main() {
  data[A] = 0; // DataIDでアクセス

  data[PIYO] = 1; // DataNameでアクセス

  for (int i = A; i < NUM; ++i) {
    data[i] = 0; // intでアクセス
  }
}

このプログラムは今は正しく動作する。するが、プログラムの規模が大きくなるにつれて保守が疎かになる。特に、配列がグローバル変数の場合は危険である。経験者であれば、公開されている部分に以下のような宣言を見つけた時の絶望感は容易に想像できるだろう。

extern int data[];

更に残念な事に、この様なレガシーコードは作り直す事が困難である場合が多い。ここで、不正アクセス等のリスクを回避する為に、添字演算子に特定の型だけを受け付ける配列の様な型を定義する。std::arrayと同様に、型と要素数を受け取り、更に添字の型として許可するものを可変長テンプレートで受け取る。後は添字演算子の引数の型が可変長テンプレート内に存在しているかどうかを調べれば良い。

#include <array>
#include <iostream>

template <class...>
struct allow_type : std::false_type {};

template <class T, class... B>
struct allow_type<T, T, B...> : std::true_type {};

template <class T, class A, class... B>
struct allow_type<T, A, B...> : allow_type<T, B...> {};

template <class T, size_t N, class... I>
struct strict_array {
  friend auto begin(strict_array& self) { return std::begin(self.array_); };
  friend auto end(strict_array& self) { return std::end(self.array_); };
  friend auto begin(strict_array const& self) { return std::begin(self.array_); };
  friend auto end(strict_array const& self) { return std::end(self.array_); };

  template <class U, std::enable_if_t<allow_type<std::decay_t<U>, I...>::value, std::nullptr_t> = nullptr>
    decltype(auto) operator [](U&& i) { return array_.at(static_cast<size_t>(std::forward<U>(i))); }

  std::array<T, N> array_;
};

int main() {
  strict_array<int, 3> a = {1,2,3}; // 添字不可
  for (auto&& i : a) { std::cout << i << std::endl; } // 範囲forは可

  strict_array<int, 3, char> b = {1,2,3}; // char
  //b[0]; // int error
  b['\0'] = 0; // char OK

  strict_array<int, 3, int, float> c = {1,2,3}; // int,float
  c[0] = 0; // int OK
  c[1.0f] = 0; // float OK

  enum class Index { A, B, C };
  strict_array<int, 3, Index> d = {1,2,3}; // scoped enum
  d[Index::A] = 0;

  struct Offset {
    explicit operator size_t() const { return value_; }
    size_t value_;
  };
  strict_array<int, 3, Offset> e = {1,2,3}; // class
  e[Offset{0}] = 0;
}

先ほどのレガシーコードでは以下の様になる。intでアクセスしていた箇所がエラーになった為、不正アクセスの可能性を検出できた。完全ではないものの、整数でのアクセスをエラーにするだけでも効果はある。

enum DataID {
  A, B, C,
  NUM
};

enum DataName {
  HOGE, FUGA, PIYO
};

strict_array<int, NUM, DataID, DataName> data = {0};

int main() {
  data[A] = 0; // DataIDでアクセス

  data[PIYO] = 1; // DataNameでアクセス

  for (auto&& it : data) { // intではアクセスできないので範囲forに置き換える
    it = 0;
  }
}

以上