[C++] 列挙体でビットフラグしたい

 C++11で導入された、型付けの強い scoped enum ですが、整数とは別の型として扱われるため、各種ビット演算等が利用できず、ビットフラグのような使い方をする場合に面倒だという話題が、少し前に職場でありました。確かに面倒ではあるのですが、楽にする手法はいくつかあります。

C++11以降における列挙体の種類

 C++11以降ではC++11以前に存在した幾つかの列挙体(以下 unscoped enum )の他に、新たに scoped enumeration (以下 scoped enum )と呼ばれる、型付けの強い列挙体が導入されました。これにより、名前空間を汚さずに列挙体を使用できるようになり、また暗黙的な整数へのキャストが行われないため、より独立した型として安全に利用できるようになりましたが、ビットフラグとして用いる際に利用する論理演算子は提供されておらず、ビットフラグとして利用しようとする場合には整数へのキャストが必要となりました。
 本稿では、unscoped enumも含め、列挙体をビットフラグとして扱う方法を考えてみたいと思います。

1. プレフィクスを付けて unscoped enum を使う(非推奨)

 よく見るコードですが、私はこの使い方が嫌いです。コーディングルールにもよりますがscoped enumと併用すると明らかに違和感が目立ちます。

enum EnumType
{
    EnumType_A = 1 << 0,
    EnumType_B = 1 << 1,
    ...
};

EnumType e = EnumType_A | EnumType_B;

のような使い方です。scoped enumと併用するとかなり気持ち悪いですが、この使い方の場合はビットフィールドである、とチーム内で合意が取れていれば、通常の列挙体との区別がつくので、ありといえばありかもしれません。私個人としては、 列挙体としてunscoped enumを使うべきではないと思っていますし、同様にこの書き方も使うべきではないと思っています。

2. マクロを使って演算子をオーバーロードする

 scoped enumに対して、必要な演算子をすべてオーバーロードする方法です。ただ、定義すべき演算子はそこそこ多いため、マクロ化して使いまわします。例えば以下のようなマクロを定義します。

#define ENUM_ATTR_BITFLAG(T)                                                    \
    constexpr T operator|(const T lhs, const T rhs)                             \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs));       \
    }                                                                           \
                                                                                \
    constexpr T operator&(const T lhs, const T rhs)                             \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs));       \
    }                                                                           \
                                                                                \
    constexpr T operator^(const T lhs, const T rhs)                             \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs));       \
    }                                                                           \
                                                                                \
    constexpr T operator~(const T val)                                          \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return static_cast<T>(~static_cast<U>(val));                            \
    }                                                                           \
                                                                                \
    inline T& operator|=(T& lhs, const T& rhs)                                  \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return lhs = static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs)); \
    }                                                                           \
                                                                                \
    inline T& operator&=(T& lhs, const T& rhs)                                  \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return lhs = static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs)); \
    }                                                                           \
                                                                                \
    inline T& operator^=(T& lhs, const T& rhs)                                  \
    {                                                                           \
        using U = typename std::underlying_type<T>::type;                       \
        return lhs = static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs)); \
    }

ビットごとの論理和、論理積、排他的論理和とそれらの複合代入演算子、および否定演算子を定義します。あとは

enum class EnumType
{
    A = 1 << 0,
    B = 1 << 1,
    ...
};
ENUM_ATTR_BITFLAG(EnumType);

EnumType e = EnumType::A | EnumType::B;

のように、scoped enum定義後に ENUM_ATTR_BITFLAG を使うことで、scoped enumに対してもビット演算が可能となります。
 欠点としては、マクロの利用による精神汚染が考えられますが、特にコードを汚染するものではないので、この方法は一種の解となり得ると思います。

3. 割り切って全てのenumの演算子をオーバーロードする(非推奨)

 上記のマクロ利用方法に似た方法ですが、こちらはテンプレートを用いて全ての列挙体の演算子をオーバーロードする方法です。マクロの利用に精神が耐えられなかった人向けですが、コード的にはマクロを使ったときより酷いことになるので利用は非推奨です。
 コードはマクロ利用時とほぼ同じです。

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr T operator|(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr T operator&(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr T operator^(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr T operator~(const T val)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(~static_cast<U>(val));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
T& operator|=(T& lhs, const T& rhs)
{
    using U = typename std::underlying_type<T>::type;
    return lhs = static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
T& operator&=(T& lhs, const T& rhs)
{
    using U = typename std::underlying_type<T>::type;
    return lhs = static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
T& operator^=(T& lhs, const T& rhs)
{
    using U = typename std::underlying_type<T>::type;
    return lhs = static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs));
}

 この定義が見えるコードでは全ての列挙体に対し集合演算が可能となってしまうため、周りのコードに対する影響が大きくなってしまいます。積極的に使用するべきではないでしょう。

4. 名前空間に閉じ込める

 C++11より前の規格のコードでよく見るパターンです。列挙体を名前空間に閉じ込めることで、スコープを限定します。

namespace EnumType
{
    enum Type
    {
        A = 1 << 0,
        B = 1 << 1,
        C = 1 << 2
    };
}

これだけです。この場合、型名が Type になってしまいます。

EnumType::Type e = EnumType::A | EnumType::B;

 もちろん名前空間ではなくクラスや構造体を使うことも出来ます。しかし、下記によりよい方法があるため、この方法よりもそちらの方法を使うほうがよいでしょう。

5. クラススコープに閉じ込める(推奨)

 C++11以前でのソリューションの一つです。クラススコープに閉じ込めることで、scoped enumと同じような使い方ができるようにするものです。

class EnumType final
{
public:
    enum Type
    {
        A = 1 << 0,
        B = 1 << 1,
        C = 1 << 2
    };
private:
    using U = typename std::underlying_type<Type>::type;
public:
    constexpr EnumType(const U val) noexcept
    : val_(static_cast<Type>(val))
    { }

    constexpr operator U() const { return val_; }
private:
    Type val_;
};

このようにすることで、以下のようにscoped enumと同じ使い方が出来ます。

EnumType e = EnumType::A | EnumType::B;

 仕組みは簡単で、外側のクラスで列挙体の基底型を引数とするコンストラクタを定義することで、クラスに列挙体を代入しているように見せかけているだけです。また、この方法でunscoped enumに対しメンバ関数をもたせることも可能です。
 ただし、利用しているのはunscoped enumであるため、整数への暗黙的なキャストは避けられません。

int a = EnumType::A; // OK
EnumType b = 0; // OK

あくまで記法をscoped enumに合わせつつ、集合演算を可能とする方法の一つとして用いるのがいいでしょう。

6. そもそも列挙体と集合は別の型(推奨: C++11〜)

 列挙体に対する集合演算の最善の解がおそらくこの方法でしょう。
 ここまでいろいろな方法を説明しておいて何ですが、そもそも列挙体をビットフラグとして扱うのは適切なことなのでしょうか。列挙体とビットフラグは、たまたま形が似ているだけで、用途は全く別にあるはずです。
 例えば、以下のコードでは関数の作者は列挙体とそのビットフラグのどちらを要求しているかわかりません。

void Hoge(const EnumType type);

列挙体とそのビットフラグを一緒くたに扱うと、ことような問題が起こってしまいます。したがって、列挙体とビットフラグは別々の型として考えるほうが安全でしょう。

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
class Set final
{
    using U = typename std::underlying_type<T>::type;
public:
    constexpr Set(const T val) noexcept
    : val_(cast(val))
    { }

    friend constexpr Set operator|(const Set lhs, const T rhs) { return Set(cast(lhs.val() | cast(rhs))); }
    friend constexpr Set operator|(const T lhs, const Set rhs) { return Set(cast(cast(lhs) | rhs.val())); }
    friend constexpr Set operator|(const Set lhs, const Set rhs) { return Set(cast(lhs.val() | rhs.val())); }
    friend constexpr Set operator&(const Set lhs, const T rhs) { return Set(cast(lhs.val() & cast(rhs))); }
    friend constexpr Set operator&(const T lhs, const Set rhs) { return Set(cast(cast(lhs) & rhs.val())); }
    friend constexpr Set operator&(const Set lhs, const Set rhs) { return Set(cast(lhs.val() & rhs.val())); }
    friend constexpr Set operator^(const Set lhs, const T rhs) { return Set(cast(lhs.val() ^ cast(rhs))); }
    friend constexpr Set operator^(const T lhs, const Set rhs) { return Set(cast(cast(lhs) ^ rhs.val())); }
    friend constexpr Set operator^(const Set lhs, const Set rhs) { return Set(cast(lhs.val() ^ rhs.val())); }
    friend constexpr bool operator==(const Set lhs, const T rhs) { return lhs.get() == rhs; }
    friend constexpr bool operator==(const T lhs, const Set rhs) { return lhs == rhs.get(); }
    friend constexpr bool operator==(const Set lhs, const Set rhs) { return lhs.get() == rhs.get(); }
    friend constexpr bool operator!=(const Set lhs, const T rhs) { return lhs.get() != rhs; }
    friend constexpr bool operator!=(const T lhs, const Set rhs) { return lhs != rhs.get(); }
    friend constexpr bool operator!=(const Set lhs, const Set rhs) { return lhs.get() != rhs.get(); }
    constexpr Set operator~() const { return Set(static_cast<T>(~val_)); }
    constexpr T get() const { return cast(val_); }

    Set& operator|=(const T rhs) { return val_ |= cast(rhs), *this; }
    Set& operator&=(const T rhs) { return val_ &= cast(rhs), *this; }
    Set& operator^=(const T rhs) { return val_ ^= cast(rhs), *this; }
private:
    U val_;

    constexpr U val() const { return static_cast<U>(val_); }
    static constexpr U cast(const T v) { return static_cast<U>(v); }
    static constexpr T cast(const U v) { return static_cast<T>(v); }
};

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr Set<T> operator|(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return Set<T>(static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs)));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr Set<T> operator&(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return Set<T>(static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs)));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr Set<T> operator^(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return Set<T>(static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs)));
}

template<
    typename T,
    typename std::enable_if<std::is_enum<T>::value, std::nullptr_t>::type = nullptr
>
constexpr Set<T> operator~(const T v)
{
    using U = typename std::underlying_type<T>::type;
    return Set<T>(static_cast<T>(~static_cast<U>(v)));
}

上記のような Set という型を定義します。列挙体に対して集合演算を行うと、この型に変化するという仕組みです。こうすることによって、関数の引数等に明示的にビットフラグを指定しなければならない、という状態を作り上げることができ、上記のような「型が表すものが曖昧になる」という問題を回避できます。

void Hoge(const EnumType type); // 列挙体そのもののみ受け付ける
void Fuga(const Set<EnumType> type); // 列挙体またはそのビットフラグ両方を受け取る

割り切って全てのenumの演算子をオーバーロードするの項目と同様、全ての列挙体にオーバーロードが定義されてしまいますが、仮に集合演算をしてしまった場合でも、 EnumTypeSet<EnumType> の型が違うため、コンパイル時に検出することが出来ます。

番外編: 特定の要素を持つenumに対し演算子をオーバーロードする(非推奨)

 最後はネタ枠です。Member Detectorイディオムをscoped enumに適用し、 BitFlag というメンバを持つ列挙体のみをビットフラグとして扱う方法です。3. 割り切って全てのenumの演算子をオーバーロードするの発展形です。

// BitFlagという定義があるかどうかをチェックする君
template<typename T>
class CheckBitFlag
{
private:
    template<typename U>
    static auto check(U v) -> decltype(U::BitFlag, void(0), std::true_type());
    static auto check(...) -> decltype(std::false_type());

    using Type = decltype(check(std::declval<T>()));
public:
    static constexpr bool Value = Type::value;
};

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
constexpr T operator|(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
constexpr T operator&(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
constexpr T operator^(const T lhs, const T rhs)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
constexpr T operator~(const T val)
{
    using U = typename std::underlying_type<T>::type;
    return static_cast<T>(~static_cast<U>(val));
}

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
T& operator|=(T& lhs, const T& rhs)
{
    using U = typename std::underlying_type<T>::type;
    return lhs = static_cast<T>(static_cast<U>(lhs) | static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
T& operator&=(T& lhs, const T& rhs)
{
    using U = typename std::underlying_type<T>::type;
    return lhs = static_cast<T>(static_cast<U>(lhs) & static_cast<U>(rhs));
}

template<
    typename T,
    typename std::enable_if<CheckBitFlag<T>::Value, std::nullptr_t>::type = nullptr
>
T& operator^=(T& lhs, const T& rhs)
{
    using U = typename std::underlying_type<T>::type;
    return lhs = static_cast<T>(static_cast<U>(lhs) ^ static_cast<U>(rhs));
}

こんな感じです。

enum class EnumType
{
    BitFlag, // 定義しておくと、オーバーロードされた演算子が使えるようになる。
    A = 0,
    B = 1,
    C = 2,
};

EnumType e = EnumType::A | EnumType::B

定義を見ればBitFlagかどうか分かりますしIDEでもコードコンプリートに表示されるので割りとありかも!?…使いみちはないと思いますがこんなこともできますよ、ということで。

まとめ

 いかがでしたでしょうか。ビットフラグの実現方法は様々ありますので、チームのルールや自分の信念と相談して、使うべき方法を決めましょう。C++11では便利なscoped enumが導入されていますので、そちらを使った手法がより安全になるのではないかと思います。C++11以降であれば、型の意味を正しく使うという意味でも6. そもそも列挙体と集合は別の型のパターンを利用することをおすすめします。

※一部コードに誤りがあったので修正しました(2018/01/16)

0 件のコメント :

コメントを投稿