enumにスコープができたはいいが使いにくい

という問題。名前被り等を気にする必要はなくなったが、 整数型への暗黙の変換も、enum 同士の計算もなにかしらを実装しなければならなくなった。 多くのひとがこれは使いにくいと思っているようで、

計算系は偉大な先輩方が強力なものを作ってくれている、が、見習い魔術師には高級言語過ぎて理解できなかったので 自分でも書いてみた。

main.cppと解説

ソース全文。ひとまず論理和だけ実装しているのだが・・・長い。

#include <iostream>
#include <type_traits> 

template < typename T > 
struct enum_cast {

  private:
  static_assert( 
      std :: is_enum< T > :: value
    , "enum_cast only used to enum type" 
  );

  public: 
  using type = T;
  using underlying_type = typename std :: underlying_type< T > :: type ;

  static constexpr underlying_type 
    to_underlying ( const type t ) { return static_cast< underlying_type >( t ) ; }
  
  static constexpr type 
    to_enum ( const underlying_type t ) { return static_cast< type >( t ) ; }
};


template < typename T, typename U  > 
using is_same_underlying = std :: is_same<
    typename enum_cast< T > :: underlying_type 
  , typename enum_cast< U > :: underlying_type 
>;



template < typename... Args> 
class is_enum_logic_safe {

  private:
   
    // no args
    template < typename... InArgs > 
      struct impl : std :: false_type {};

    // single arg
    template < typename Type1, typename... Tail > 
      struct impl< Type1, Tail... > : std :: true_type {};

    // more than 2 args
    template < typename Type1, typename Type2, typename... Tail > 
      struct impl< Type1, Type2, Tail... > : 
      std :: conditional<
          is_same_underlying< Type1, Type2 > :: value 
        , typename impl< Type2, Tail... > :: type 
        , std :: false_type 
      > :: type  
      {};

  public:

    static constexpr bool value = impl< Args... > :: type :: value;
};




template < typename T, typename U = T > 
struct enum_logic_enabled : std :: false_type {}; 


template < typename T, typename U > 
using enum_enabler = std :: enable_if<
    enum_logic_enabled< T, U > :: value &&
    is_enum_logic_safe< T, U > :: value 
  , std :: nullptr_t
>;




template < 
    typename T
  , typename U = T
  , typename enum_enabler< T, U > :: type = nullptr 
> 
static constexpr T operator| ( T t, U u ) {
  return enum_cast< T > :: to_enum( 
    enum_cast< T > :: to_underlying( t ) | enum_cast< U > :: to_underlying( u ) 
  ) ;
}







enum class A : unsigned char { 
  one
};

enum class B : unsigned char {
  two
};

enum class C : unsigned int {
  three
};


template <> 
struct enum_logic_enabled< A > : std :: true_type {};

template <> 
struct enum_logic_enabled< C > : std :: true_type {};

template <> 
struct enum_logic_enabled< A, C > : std :: true_type {};

template <> 
struct enum_logic_enabled< A, B > : std :: true_type {};


int main(int argc, const char * argv[]) {

  A a;
  B b;
  C c;

  a = A :: one   | A :: one;
  //b = B :: two   | B :: two; 
  c = C :: three | C :: three;


  a = A :: one | B :: two ;
  //a = A :: one | C :: three; 
  
  unsigned char uc = 0;
  int i = 0;

  //enum_cast< int > :: type j = 0;


  


  return 0;
}

enum_cast

template < typename T > 
struct enum_cast {

  private:
  static_assert( std :: is_enum< T > :: value , "enum_cast only used to enum type" );

  public: 
  using type = T;
  using underlying_type = typename std :: underlying_type< T > :: type ;

  static constexpr underlying_type 
    to_underlying ( const type t ) { return static_cast< underlying_type >( t ) ; }
  
  static constexpr type 
    to_enum ( const underlying_type t ) { return static_cast< type >( t ) ; }
};
enum_cast<T> :: type
テンプレート引数で与えられた enum class T
enum_cast<T> :: underlying_type
テンプレート引数で与えられた enum class T の基底型
enum_cast<T> :: to_underlying(const type t)
enum class T を引数にとり、enum class T の基底型を返す
enum_cast<T> :: to_enum(const underlying_type t)
enum class T の基底型を引数にとり、enum class T 型を返す

他に

private:
static_assert( std :: is_enum< T > :: value , "enum_cast only used to enum type" );

T が enum class でない場合、コンパイル時に enum_cast only used to enum type とエラーを吐いて終了する。

static constexpr T operator| ( T t, U u ) {
  return enum_cast< T > :: to_enum( //enum class 型に戻す・・・(2)
    enum_cast< T > :: to_underlying( t ) | enum_cast< U > :: to_underlying( u )  //enum classの基底型にして計算・・・(1)
  ) ;

このように、演算を行う際はこの2つを組み合わせて利用する。

is_same_underlying

template < typename T, typename U  > 
using is_same_underlying = std :: is_same<
    typename enum_cast< T > :: underlying_type 
  , typename enum_cast< U > :: underlying_type 
>;

2つの enum class を受け取って基底型が同一か確認する。 基底型の取得には enum_cast<T> :: underlying_type を利用している。 なので、T / U が enum class でない場合、エラーとなる。

今回2つの enum class 同士の計算ができるように作っているが、それは

enum class status : unsigned char{
    off      = 0b00000000
  , standby  = 0b00000001
  , running  = 0b00000010
  , abort    = 0b00000011
  , overflow = 0b10000000
};

enum class mask : unsigned char {
    overflow = 0b10000000
  , status   = 0b00000011
};

このような enum class 同士の計算を想定している。

is_enum_logic_safe

template < typename... Args> 
class is_enum_logic_safe {

  private:
   
    // no args
    template < typename... InArgs > 
      struct impl : std :: false_type {};

    // single arg
    template < typename Type1, typename... Tail > 
      struct impl< Type1, Tail... > : std :: true_type {};

    // more than 2 args
    template < typename Type1, typename Type2, typename... Tail > 
      struct impl< Type1, Type2, Tail... > : 
      std :: conditional<
          is_same_underlying< Type1, Type2 > :: value 
        , typename impl< Type2, Tail... > :: type 
        , std :: false_type 
      > :: type  
      {};

  public:

    static constexpr bool value = impl< Args... > :: type :: value;
};

与えられたテンプレート引数同士で計算して問題がないかチェックする。

// no args
template < typename... InArgs > 
  struct impl : std :: false_type {};

引数が何もない状態では計算も何もないので std :: false_type

// more than 2 args
template < typename Type1, typename Type2, typename... Tail > 
  struct impl< Type1, Type2, Tail... > : 
  std :: conditional<
      is_same_underlying< Type1, Type2 > :: value 
    , typename impl< Type2, Tail... > :: type 
    , std :: false_type 
  > :: type  
  {};

引数が2つ以上の場合に呼ばれる。 先頭から順番に enum class の基底型が同一かをチェックする動作を実装した。

1つ目と2つ目、 enum class Type1 / Type2 の基底型が同一だった場合、 Type2 を先頭にしたパラメータパックでもう一度 impl(...) を呼び出す、 という処理が引数1つになるまで続く。

// single arg
template < typename Type1, typename... Tail > 
  struct impl< Type1, Tail... > : std :: true_type {};
最初から引数が1つしか与えられなかった場合
基底型が一緒なのは自明なので std :: true_type
最後の1つまで基底型が一緒だった場合
ここでの Type1 = 前ループのType2、 つまり最後の引数まで全て基底型が同一だったことになるので std :: true_type を返してあげなければならない
public:
static constexpr bool value = impl< Args... > :: type :: value;

処理結果を value として定義。これが返り値になる。

enum_enabler

template < typename T, typename U > 
using enum_enabler = std :: enable_if<
    enum_logic_enabled< T, U > :: value &&
    is_enum_logic_safe< T, U > :: value 
  , std :: nullptr_t
>;

各種 operator の第3引数に指定され、

template < typename T , typename U = T, typename enum_enabler< T, U > :: type = nullptr > 

関数の有効/無効を制御する。 std :: enable_if

std :: enable_if<
  /* 条件 */
  /* trueの時の型 */
> :: type /* trueの場合のみ type が存在する */

というテンプレートメタ関数。なので

enum class T / U 間の計算を有効化(enum_logic_enabled< T, U > :: value) されており(後述)、かつ enum class T / U 間で計算して問題ない(is_enum_logic_safe< T, U > :: value)時に限り、

template < typename T , typename U = T, typename std :: nullptr_t = nullptr >

となり関数が有効化する。もし条件に合わない場合、

template < typename T , typename U = T>

の探索が始まるが、定義されていない場合はエラーとなる。

typename U = T

テンプレート引数が1つだけの場合、U には T が代入されるので、T 同士の計算となる。

enum_logic_enabled

template < typename T, typename U = T > 
struct enum_logic_enabled : std :: false_type {}; 

enum class 型 TU 間の計算の有効/無効を定義する。タグディスパッチと呼ばれる手法。 有効化したい組み合わせを

template <> 
struct enum_logic_enabled< A > : std :: true_type {};

template <> 
struct enum_logic_enabled< C > : std :: true_type {};

template <> 
struct enum_logic_enabled< A, C > : std :: true_type {};

template <> 
struct enum_logic_enabled< A, B > : std :: true_type {};

このように std :: true_type を継承させて特殊化させることで、 上記 enum_enabler の条件式が変化する。

ただし、有効化させても計算に問題がある( is_enum_logic_safe<T, U> :: value = false ) 場合、 関数は有効にならない。

operator

template < typename T , typename U = T, typename enum_enabler< T, U > :: type = nullptr > 
static constexpr T operator| ( T t, U u ) {
  return enum_cast< T > :: to_enum( 
    enum_cast< T > :: to_underlying( t ) | enum_cast< U > :: to_underlying( u ) 
  ) ;
}

ところどころバラバラになって出てきたがこのように書く。 他の演算子を追加するには、例えば & を追加する場合、

template < typename T , typename U = T, typename enum_enabler< T, U > :: type = nullptr > 
static constexpr T operator& ( T t, U u ) {
  return enum_cast< T > :: to_enum( 
    enum_cast< T > :: to_underlying( t ) & enum_cast< U > :: to_underlying( u ) 
  ) ;
}

このようにするだけで有効化している組み合わせ全てで operator& の計算が有効化される。

その他

テストケースの enum class

enum class A : unsigned char { 
  one
};

enum class B : unsigned char {
  two
};

enum class C : unsigned int {
  three
};

main 関数内

  A a;
  B b;
  C c;

  a = A :: one   | A :: one;
  //b = B :: two   | B :: two; 
  //Bの計算が有効化されていないためエラー
  c = C :: three | C :: three;


  a = A :: one | B :: two ;
  //a = A :: one | C :: three; 
  //A - C 間で計算は有効化されているが基底型が違うのでエラー
  
  unsigned char uc = 0;
  int i = 0;

  //enum_cast< int > :: type j = 0;
  //int は enum class ではないのでエラー

出力結果

なし

連絡事項

namespace に入れるなどは各自で。