enumを基に変換関数を内蔵したオブジェクトをつくる

enumで定義されたデータをシリアライズするときは、何らかの数値(あるいは文字列)と相互変換する。
相手がintでC言語であれば、単純にキャストで相互変換もできる。C++であればstatic_castを使うこともあるだろう。

  enum en { e1, e2, e3 };
  
  en e = e3;
  int n = (int)e; // C言語、C++言語
  int m = static_cast<int>(e); // C++言語

  int s = 2;
  en f = (en)s; // e3
  en g = static_cast<en>(s); // e3

但し、enumシリアライズでつきものなのは、その仕様が変わった場合、互換性が損なわれることや、範囲外の値のチェックについて、メンテナンス性に欠けるといった点だ。

  enum en { e1, e4, e2, e3 }; // 仕様が変わった
  
  en e = e2;
  int n = (int)e; // C言語、C++言語
  int m = static_cast<int>(e); // C++言語

  int s = 2;
  en f = (en)s; // e2!
  en g = static_cast<en>(s); // e2!

実用的なコードを記述するときに、シリアライズでチェックするコードとenumの宣言が別のファイルで記述されることはよくあることで、enumの定義を変えたときに、対応するコードを更新し忘れすることは良くあることだ。
そのようなことにならないよう、enumの宣言で定数定義する方法もある。

  enum en { e1=0, e4=3, e2=1, e3=2 }; // 定数定義

しかし、古いプログラムでe4がどう扱われるか? と言った問題がある。enumで定義が大量にある場合などは悪夢だろう。

そういう点で、enumと例えばintの相互変換をenumの宣言の近くに配置できれば、メンテナンス性が向上することが期待できる。

前回の方法で、enumとintの相互変換ができる元のenumと互換なオブジェクトを定義してみる。
もちろん、変換関数を双方向分、enumの定義の近くで定義しても良いが、その方法は簡単なので割愛する。

template <typename T>
class TSetFunc
{
  public:
    static int to_int( T   a ) { return static_cast<int>(a); }
    static T   to_T(   int n ) { return static_cast<T>(n); }
};

template <typename T, class C>
class TBasicSetParam
{
  private:
    T t;
  public:
    TBasicSetParam() : t() {}
    TBasicSetParam(            const T                lhs ) { t = lhs; }
    TBasicSetParam(            const int              lhs ) { t = C::to_T( lhs ); }
    TBasicSetParam(            const TBasicSetParam&  lhs ) = default;
    TBasicSetParam(                  TBasicSetParam&& rhs ) = default;
    TBasicSetParam& operator=( const TBasicSetParam&  lhs ) = default;
    TBasicSetParam& operator=(       TBasicSetParam&& rhs ) = default;
    ~TBasicSetParam()                                       = default;

    operator T() { return t; }
    int to_int() { return C::to_int( t ); }
};
template <typename T>
using TSetParam = TBasicSetParam<T, TSetFunc<T>>;

enumを定義して、その変換オブジェクトを実装する。

enum class ec { e1, e2, e3 };

template<>
class TSetFunc<ec>
{
  public:
    static int to_int( ec a ) {
      switch( a )
      {
        case ec::e1: return  1;
        case ec::e2: return  2;
        case ec::e3: return  3;
        default:     return -1; // throw false;とすれば例外を投げることも
      }
    }
    static ec to_T( int n ) {
      switch( n )
      {
        case  1: return ec::e1;
        case  2: return ec::e2;
        case  3: return ec::e3;
        default: return ec::e1;
      }
    }
};
using Ec = TSetParam<ec>;

利用コードは、以下のような感じになる。

  Ec E;

  E = ec::e1;
  E = 2;
  if( E == ec::e2 )
    E = ec::e1;
  if( ec::e1 == E )
    E = ec::e3;

  int n = E.to_int();
  ec e = E;

enumの定義外もdefaultでどうするか決めることができる。
enumからintへのswitch文でわざとdefaultを定義しなければ、全てのcaseが記述されなければコンパイル時に警告を発生させることもできる。
また、オブジェクトEcは元のecのオブジェクトとほぼ同等に扱える。
switchの遅さが気になるのであれば、別に早いコードに書き換えることも可能だ。

intの代入は副作用が怖いので、TBasicSetParam( const int lhs )を定義しないで、staticな変換関数を準備する方が良いかもしれない。

template <typename T, class C>
class TBasicSetParam
{
  private:
    T t;
  public:
    TBasicSetParam() : t() {}
    TBasicSetParam(            const T                lhs ) { t = lhs; }
    //TBasicSetParam(            const int              lhs ) { t = C::to_T( lhs ); }
    static TBasicSetParam int_to( const int lhs ) { return TBasicSetParam( C::to_T(lhs) ); }
    TBasicSetParam(            const TBasicSetParam&  lhs ) = default;
    TBasicSetParam(                  TBasicSetParam&& rhs ) = default;
    TBasicSetParam& operator=( const TBasicSetParam&  lhs ) = default;
    TBasicSetParam& operator=(       TBasicSetParam&& rhs ) = default;
    ~TBasicSetParam()                                       = default;

    operator T() { return t; }
    int to_int() { return C::to_int( t ); }
};
template <typename T>
using TSetParam = TBasicSetParam<T, TParamFunc<T>>;
  Ec E;

  E = Ec::int_to(2);