【C/C++】列挙体を継承するテクニック

C言語、C++、いずれの言語も列挙体を継承することは出来ません。

enum X { A, B, C };
enum Y : X { D, E, F }; // ERROR: Non-integral type 'X' is an invalid underlying type

代替案としては、プリプロセッサの利用や列挙定数の数珠つなぎ、列挙体を構造体でラップ方法、構造体の定数を定義する方法等、様々な方法が考えられます。

インクルード・ファイル

親列挙体の要素を外部ファイルに定義する方法を紹介します。 定義された要素はプリプロセッサの#includeで動的に埋め込みます。

/usr/local/misc/X.def

A, B, C,

/usr/local/src/main.c

enum Y {
  #include "/usr/local/misc/X.def"
  D, E, F,
};

int main() {
  enum Y a = D;
  enum Y b = A; // OK
}

C言語、C++、いずれの言語でも利用可能なイディオムです。要素名の再利用を目的とする場合に有効な方法です。

この列挙体の要素(列挙定数/列挙子)を外部の定義ファイルで管理するテクニック自体は、低レイヤーの世界や伝統的なオープンソース・プロジェクトでよく見かけます。要素数が多い場合や要素が頻繁に入れ替わるようなケースでは有効的なテクニックですが、今回のような用途では少し大げさ過ぎるかもしれません。

列挙定数チェーン

列挙定数の連番を保証するためのテクニックです。

enum X {
  A,
  B,
  C,
};
enum Y {
  D = C + 1,
  E,
  F
};

保守性を考慮する場合は次のように、親列挙体Xの最終要素を表す列挙定数end_of_enum_Xを事前に定義しておくと良いでしょう。

enum X {
  S,
  A, AA, AAA,
  B,
  C,
  end_of_enum_X = C,
};
enum Y {
  D = end_of_enum_X + 1,
  E,
  F
};

end_of_enum_X = Cとすることで、end_of_enum_Xに対するswitch文の余計な欠落チェックを回避することが出来ます。

C++であれば、この発想を応用してより安全性の高い方法を実現することが可能です。次の構造体Enumを参考にしてください。

構造体Enum

列挙体を構造体でラップする方法です。列挙型の挙動を構造体で柔軟に制御します。先程紹介した# 列挙定数チェーンのアイディアを応用しています。

/* 親 */
struct C {     // 構造体
   enum Type { // 列挙体
      Int, Float,
      Type_end_of_C = Float
   } _type;
   // 列挙型 → 構造体型
   C(int type) : _type(static_cast<Type>(type)) {}
   // 構造体型 → 列挙型
   Type type() const { return _type; }
   // 比較
   friend bool operator==(const C& L, const C& R) { return L.type() == R.type(); }
};

/* 子 */
struct Cpp : C {
   using C::C;
   enum Type {
      String = 1 + Type_end_of_C,
      Vector,
      // Type_end_of_Cpp
   };
   Type type() const { return static_cast<Type>(this->_type); }
};

使用例は以下の通りです。

/* 初期化 */
C   a = Cpp::Int;
Cpp b = C::Int;
Cpp c = Cpp::Vector;
Cpp d = 3/*Cpp::Vector*/;

/* 比較 */
std::cout << (c == d); // true
std::cout << (a == b); // true
std::cout << (a == c); // false

/* 型の安全性 */

// 警告:Comparison of two values with different enumeration types ('C::Type' and 'Cpp::Type')
std::cout << (a.type() == b.type());    // true
std::cout << (a.type() == b.C::type()); // true
std::cout << (a.type() == c.C::type()); // false

C ccc = C::Int;
switch (ccc.type()) {
   case C::Int: puts("C::Int"); break;
   // 省略時警告:Enumeration value 'Float' not handled in switch
   default: break;
}

Cpp cpp = Cpp::Int;
switch (cpp.type()) {
   case Cpp::String: puts("Cpp::String"); break;
   // 警告:Case value not in enumerated type 'Cpp::Type'
   case C::Int: puts("C::Int"); break;
   // 省略時警告:Enumeration value 'Vector' not handled in switch
   case Cpp::Vector: puts("Cpp::Vector"); break;
}
switch (cpp.C::type()) {
   case C::Int:   puts("C::Int");   break;
   case C::Float: puts("C::Float"); break;
   // 警告:Case value not in enumerated type 'C::Type'
   case Cpp::String: puts("Cpp::String"); break;
}

構造体定数

列挙定数を構造体型の定数値として表現する方法です。 実装方法は色々あります。以下はその一例です。

struct Prog {
  const int rawValue;
  constexpr Prog(int v) : rawValue(v) {}
  constexpr operator int() const { return rawValue; }
};
struct A : Prog {
  static constexpr Prog ABC{0};
  static constexpr Prog Ada{1};
};
struct B : A {
  static constexpr Prog Bash{3};
  static constexpr Prog BASIC{4};
};

switch文における列挙定数の欠落チェックが行われないという問題があります。

constexpr Prog b = B::ABC;
switch (b) {
  case B::ABC: puts("ABC"); break;
  // AdaやBASIC等の欠落がチェックされない
  // defaultも省略できてしまう
}

また列挙子の連続性を保証できないという問題があります。そもそもこちらの方法では列挙体を使用しておらず、構造体定数Progの内部値もプログラマが手動で指定する必要があります。

広告