【C++】列挙型をビット演算する方法【ビットマスク処理・整数演算】

通常の列挙型(enum)とスコープ付きの列挙型クラス(enum class)でそれぞれ異なる方法を取る必要があります。「enum構造体」を活用した裏テクニックもあります。

列挙型

C言語由来の古くから使われている列挙体であれば、特に意識することなくANDやOR等の演算が可能ですが、演算結果を列挙体の変数に格納する際にはキャストが必要になります。

enum Animal {
  Cat = 1 << 0,
  Dog = 1 << 1,
};

// ERROR: Cannot initialize a variable of type 'Animal' with an rvalue of type 'int'
Animal a = Animal::Cat | Animal::Dog;

// キャストが必要
Animal a = static_cast<Animal>(Animal::Cat | Animal::Dog);

// 演算は普通に可能
if (a & Animal::Cat) puts("cat");
if (a & Animal::Dog) puts("dog");

列挙型クラス

列挙型クラス/スコープ付き列挙型の場合は型に対する扱いが厳密になっていますので、全ての処理で型キャストが必須です。今回は明示的な型キャストを行うために任意の型uint64_tを指定しています。

enum class Animal : uint64_t {
  Cat = 1 << 0,
  Dog = 1 << 1,
};

// ERROR: Invalid operands to binary expression ('Animal' and 'Animal')
Animal a = Animal::Cat | Animal::Dog;

// 演算時もキャストが必要
Animal a = (Animal)((uint64_t)Animal::Cat | (uint64_t)Animal::Dog);

if ((uint64_t)a & (uint64_t)Animal::Cat) puts("cat");
if ((uint64_t)a & (uint64_t)Animal::Dog) puts("dog");

実際の開発ではCスタイルのキャスト(型名)値を使わずC++スタイルでstatic_cast<型名>(値)と書くようにしましょう。

演算用の演算子を独自定義する

キャストが面倒な場合は、ビット演算用の演算子を独自定義すると便利です。今回は例としてANDとORの演算子を定義してみます。

enum class Animal : uint64_t { Cat = 1 << 0, Dog = 1 << 1 };
Animal operator|(Animal L, Animal R) {
  return static_cast<Animal>(static_cast<uint64_t>(L) | static_cast<uint64_t>(R));
}
Animal operator&(Animal L, Animal R) {
  return static_cast<Animal>(static_cast<uint64_t>(L) & static_cast<uint64_t>(R));
}
// キャストが不要になる
Animal a = Animal::Cat | Animal::Dog;
if ((a & Animal::Cat) == Animal::Cat) puts("cat");
if ((a & Animal::Dog) != static_cast<Animal>(0)) puts("dog");

汎用化

今回Animal型キャストの際に、内部型uint64_tを手動で指定していましたが、underlying_typeで列挙体の内部型を動的に取得することも可能です。

#include <type_traits>
enum class Animal : uint64_t {}
std::underlying_type<Animal>::type // uint64_t

enum class Animal {}
std::underlying_type<Animal>::type // int

enum構造体

以前の記事(C++の列挙型でメンバ関数を実現する方法)で列挙型を構造体でラップするテクニックを紹介しましたが、このアイディアを応用すると、全ての演算処理に対応出来る超柔軟型を実現することが可能になります。

struct Flags {
  enum Type : unsigned int {
    A = 1 << 0, // 1 (0b0001)
    B = 1 << 1, // 2 (0b0010)
    C = 1 << 2, // 4 (0b0100)
  } _type;
  Flags(unsigned int v) : _type(static_cast<Type>(v)) {}
  operator Type() { return _type; }
};
Flags a = Flags::A;
Flags ab = a | Flags::B;
if (a != ab) printf("%d, %d, ", +a, +ab);  // 1, 3
if (a & ab)  printf("%d", ab & Flags::B);  // 2
if ((Flags::A << 1) & Flags::B) puts("yes"); // yes

operator Type()で構造体から列挙型への暗黙型変換を容認している所が、今回の肝です。構造体から暗黙変換された列挙型は更に整数型へ暗黙変換され、結果として整数演算・ビット演算が可能になるというカラクリです。

変換コンストラクタFlags(unsigned int v)は整数型の演算結果を再度構造体に変換し直すために使われています。

使うか使わないかはあなた次第です。

広告