C ++でビットフラグにスコープ付き列挙型を使用する


60

enum X : int(C#)と、またはenum class X : int(C ++ 11)の隠れた内部フィールド有するタイプであるintことは、任意の値を保持することができます。さらに、いくつかの定義済み定数がX列挙型で定義されています。列挙型を整数値にキャストしたり、その逆を行うことができます。これは、C#とC ++ 11の両方に当てはまります。

C#では、列挙型は個々の値を保持するためだけでなく、Microsoftの推奨に従って、フラグのビットごとの組み合わせを保持するためにも使用されます。このような列挙型は(通常、必ずしもそうではありませんが)[Flags]属性で装飾されています。開発者の生活を楽にするために、ビット単位の演算子(OR、ANDなど)がオーバーロードされているため、次のようなことが簡単にできます(C#)。

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

私は経験豊富なC#開発者ですが、C ++をプログラミングしてから数日しか経っていないため、C ++の規約については知りません。C#で使用したのとまったく同じ方法でC ++ 11列挙型を使用する予定です。C ++ 11では、スコープ付き列挙型のビット演算子はオーバーロードされないため、それらをオーバーロードしたかったのです。

これは議論を呼び起こし、意見は3つの選択肢の間で異なるようです:

  1. C#と同様に、ビット型を保持するために列挙型の変数が使用されます。

    void M(NumericType flags);
    
    // With operator overloading:
    M(NumericType::Sign | NumericType::ZeroPadding);
    
    // Without operator overloading:
    M(static_cast<NumericType>(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding)));

    しかし、これはC ++ 11のスコープ付き列挙型の厳密に型指定された列挙型の哲学に反するでしょう。

  2. 列挙型のビットごとの組み合わせを保存する場合は、プレーン整数を使用します。

    void M(int flags);
    
    M(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding));

    しかし、これはすべてをに減らしint、メソッドにどのタイプを入れるべきかについての手がかりを与えません。

  3. 演算子をオーバーロードし、非表示の整数フィールドにビット単位のフラグを保持する別のクラスを作成します。

    class NumericTypeFlags {
        unsigned flags_;
    public:
        NumericTypeFlags () : flags_(0) {}
        NumericTypeFlags (NumericType t) : flags_(static_cast<unsigned>(t)) {}
        //...define BITWISE test/set operations
    };
    
    void M(NumericTypeFlags flags);
    
    M(NumericType::Sign | NumericType::ZeroPadding);

    user315052による完全なコード

    しかし、その場合、可能な値を示唆するIntelliSenseなどのサポートはありません。

これは主観的な質問であることは知っていますが、どのアプローチを使用すればよいですか?C ++で最も広く認識されているのは、もしあれば、どのようなアプローチですか?ビットフィールドを扱うとき、どのようなアプローチを使用します

もちろん、3つのアプローチすべてが機能するため、単に個人的な好みではなく、事実上および技術上の理由、一般に受け入れられている慣習を探しています。

たとえば、C#のバックグラウンドのため、C ++のアプローチ1を使用する傾向があります。これには、私の開発環境が可能な値を教えてくれるという追加の利点があり、オーバーロードされた列挙演算子を使用すると、これは簡単に記述して理解でき、非常にきれいです。また、メソッドのシグネチャは、期待される値の種類を明確に示しています。しかし、ここにいるほとんどの人は、おそらく正当な理由で、私に反対しています。


2
ISO C ++委員会は、列挙の値の範囲にフラグのすべてのバイナリの組み合わせが含まれることを明示的に述べるのに十分なオプション1が重要であると判断しました。(これはC ++ 03より前のことです)したがって、このやや主観的な質問に対する客観的な承認があります。
–MSalters

1
(@MSaltersのコメントを明確にするために、C ++列挙型の範囲は、基礎となる型(固定型の場合)またはそうでなければ列挙子に基づきます。後者の場合、範囲は定義されたすべての列挙子を保持できる最小のビットフィールドに基づきます;例えば、のためにenum E { A = 1, B = 2, C = 4, };、範囲は0..7(3ビット)このように、C ++標準は、明示的に#1は常に実行可能な選択肢となることを保証【具体的には、。enum classデフォルトenum class : int特に指定し、したがって常に一定下地タイプを持っていない限り。])
ジャスティンタイム

回答:


31

最も簡単な方法は、オペレーターに自分でオーバーロードを提供することです。型ごとの基本的なオーバーロードを拡張するマクロを作成することを考えています。

#include <type_traits>

enum class SBJFrameDrag
{
    None = 0x00,
    Top = 0x01,
    Left = 0x02,
    Bottom = 0x04,
    Right = 0x08,
};

inline SBJFrameDrag operator | (SBJFrameDrag lhs, SBJFrameDrag rhs)
{
    using T = std::underlying_type_t <SBJFrameDrag>;
    return static_cast<SBJFrameDrag>(static_cast<T>(lhs) | static_cast<T>(rhs));
}

inline SBJFrameDrag& operator |= (SBJFrameDrag& lhs, SBJFrameDrag rhs)
{
    lhs = lhs | rhs;
    return lhs;
}

(これtype_traitsはC ++ 11ヘッダーでstd::underlying_type_tあり、C ++ 14の機能であることに注意してください。)


6
std :: underlying_type_tはC ++ 14です。C ++ 11ではstd :: underlying_type <T> :: typeを使用できます。
ddevienne

14
なぜstatic_cast<T>入力に使用していますが、ここでは結果にCスタイルのキャストを使用していますか?
ルスラン

2
@Ruslan 2番目にこの質問
audiFanatic

intであることがすでにわかっているのに、なぜstd :: underlying_type_tに悩まされているのですか?
poizan42

1
場合はSBJFrameDrag、クラスで定義され、|演算子は、後で同じクラスの定義で使用され、どのようにそれがクラス内で使用できるようにオペレータを定義するのでしょうか?
HelloGoodbye

6

歴史的に、私は常に古い(弱い型付けの)列挙を使用してビット定数に名前を付け、結果として生じるフラグを格納するためにストレージクラスを明示的に使用していました。ここでは、列挙がストレージタイプに収まるようにし、フィールドとそれに関連する定数との関連付けを追跡する責任があります。

厳密に型指定された列挙型のアイデアは好きですが、列挙型の変数にはその列挙型の定数に含まれない値が含まれる可能性があるという考えにはあまり慣れていません。

たとえば、ビット単位またはオーバーロードされていると仮定すると:

enum class E1 { A=1, B=2, C=4 };
void test(E1 e) {
    switch(e) {
    case E1::A: do_a(); break;
    case E1::B: do_b(); break;
    case E1::C: do_c(); break;
    default:
        illegal_value();
    }
}
// ...
test(E1::A); // ok
test(E1::A | E1::B); // nope

3番目のオプションでは、列挙のストレージタイプを抽出するためのボイラープレートが必要です。符号なしの基になる型を強制したい場合(もう少しコードを使用して符号付きも処理できます):

template <size_t Size> struct IntegralTypeLookup;
template <> struct IntegralTypeLookup<sizeof(int64_t)> { typedef uint64_t Type; };
template <> struct IntegralTypeLookup<sizeof(int32_t)> { typedef uint32_t Type; };
template <> struct IntegralTypeLookup<sizeof(int16_t)> { typedef uint16_t Type; };
template <> struct IntegralTypeLookup<sizeof(int8_t)>  { typedef uint8_t Type; };

template <typename IntegralType> struct Integral {
    typedef typename IntegralTypeLookup<sizeof(IntegralType)>::Type Type;
};

template <typename ENUM> class EnumeratedFlags {
    typedef typename Integral<ENUM>::Type RawType;
    RawType raw;
public:
    EnumeratedFlags() : raw() {}
    EnumeratedFlags(EnumeratedFlags const&) = default;

    void set(ENUM e)   { raw |=  static_cast<RawType>(e); }
    void reset(ENUM e) { raw &= ~static_cast<RawType>(e); };
    bool test(ENUM e) const { return raw & static_cast<RawType>(e); }

    RawType raw_value() const { return raw; }
};
enum class E2: uint8_t { A=1, B=2, C=4 };
typedef EnumeratedFlags<E2> E2Flag;

それでも、IntelliSenseやオートコンプリートは提供されませんが、ストレージタイプの検出は、当初の予想よりも見苦しくありません。


今、私は別の方法を見つけました:弱い型の列挙のストレージタイプを指定できます。C#と同じ構文もあります

enum E4 : int { ... };

型が弱く、暗黙的にint(または選択したストレージタイプ)との間で変換されるため、列挙された定数と一致しない値を使用することは奇妙ではありません。

欠点は、これが「過渡的」として記述されることです...

NB。このバリアントは、列挙された定数をネストされたスコープと囲んでいるスコープの両方に追加しますが、ネームスペースでこれを回避できます:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A

1
弱く型付けされた列挙型の別の欠点は、それらの定数が列挙型名の接頭辞を付ける必要がないため、定数が私の名前空間を汚染することです。また、同じ名前のメンバーを持つ2つの異なる列挙型がある場合、すべての種類の奇妙な動作を引き起こす可能性があります。
ダニエルAA Pelsmaeker

それは本当だ。指定されたストレージタイプを持つ弱く型付けされたバリアントは、そのスコープ囲んでいるスコープ独自のスコープiiucの両方に追加します。
役に立たない

スコープ外の列挙子は、周囲のスコープでのみ宣言されます。enum-nameで修飾できることは、ルックアップルールの一部であり、宣言ではありません。C ++ 11 7.2 / 10:各enum-nameおよび各スコープ外列挙子は、enum-specifierをすぐに含むスコープで宣言されます。各スコープ付き列挙子は、列挙のスコープで宣言されます。これらの名前は、(3.3)および(3.4)のすべての名前に対して定義されたスコープルールに従います。
ラースヴィクルンド14年

1
C ++ 11では、列挙型の基本型を提供するstd :: underlying_typeがあります。したがって、 'template <typename IntegralType> struct Integral {typedef typename std :: underlying_type <IntegralType> :: type Type; }; `C ++ 14では、これらはさらに単純化されています 'template <typename IntegralType> struct Integral {typedef std :: underlying_type_t <IntegralType> Type; };
emsr 14年

4

を使用して、C ++ 11で型保証列挙フラグを定義できますstd::enable_if。これは初歩的な実装であり、いくつかのことが欠けている可能性があります。

template<typename Enum, bool IsEnum = std::is_enum<Enum>::value>
class bitflag;

template<typename Enum>
class bitflag<Enum, true>
{
public:
  constexpr const static int number_of_bits = std::numeric_limits<typename std::underlying_type<Enum>::type>::digits;

  constexpr bitflag() = default;
  constexpr bitflag(Enum value) : bits(1 << static_cast<std::size_t>(value)) {}
  constexpr bitflag(const bitflag& other) : bits(other.bits) {}

  constexpr bitflag operator|(Enum value) const { bitflag result = *this; result.bits |= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator&(Enum value) const { bitflag result = *this; result.bits &= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator^(Enum value) const { bitflag result = *this; result.bits ^= 1 << static_cast<std::size_t>(value); return result; }
  constexpr bitflag operator~() const { bitflag result = *this; result.bits.flip(); return result; }

  constexpr bitflag& operator|=(Enum value) { bits |= 1 << static_cast<std::size_t>(value); return *this; }
  constexpr bitflag& operator&=(Enum value) { bits &= 1 << static_cast<std::size_t>(value); return *this; }
  constexpr bitflag& operator^=(Enum value) { bits ^= 1 << static_cast<std::size_t>(value); return *this; }

  constexpr bool any() const { return bits.any(); }
  constexpr bool all() const { return bits.all(); }
  constexpr bool none() const { return bits.none(); }
  constexpr operator bool() { return any(); }

  constexpr bool test(Enum value) const { return bits.test(1 << static_cast<std::size_t>(value)); }
  constexpr void set(Enum value) { bits.set(1 << static_cast<std::size_t>(value)); }
  constexpr void unset(Enum value) { bits.reset(1 << static_cast<std::size_t>(value)); }

private:
  std::bitset<number_of_bits> bits;
};

template<typename Enum>
constexpr typename std::enable_if<std::is_enum<Enum>::value, bitflag<Enum>>::type operator|(Enum left, Enum right)
{
  return bitflag<Enum>(left) | right;
}
template<typename Enum>
constexpr typename std::enable_if<std::is_enum<Enum>::value, bitflag<Enum>>::type operator&(Enum left, Enum right)
{
  return bitflag<Enum>(left) & right;
}
template<typename Enum>
constexpr typename std::enable_if_t<std::is_enum<Enum>::value, bitflag<Enum>>::type operator^(Enum left, Enum right)
{
  return bitflag<Enum>(left) ^ right;
}

number_of_bits残念ながら、C ++には列挙の可能な値を内省する方法がないため、コンパイラによって埋められないことに注意してください。

編集:実際に私は修正されたままで、コンパイラを埋めることは可能number_of_bitsです。

これは、非連続的な列挙値の範囲を(非常に非効率的に)処理できることに注意してください。このような列挙型で上記を使用するのは良い考えではないと言ってみましょう。そうしないと狂気が続きます:

enum class wild_range { start = 0, end = 999999999 };

しかし、これは最終的に非常に有用なソリューションであるとみなされます。ユーザー側のビットフィドリングを必要とせず、タイプセーフであり、その範囲内で、可能な限り効率的です(std::bitsetここで実装品質に強く傾倒しています;))。


演算子のオーバーロードを見逃したと確信しています。
rubenvb 16

2

嫌い 私のC ++ 14のマクロを次の人と同じくらい嫌いますが、私はこれをあちこちで使用するようになりました。

#define ENUM_FLAG_OPERATOR(T,X) inline T operator X (T lhs, T rhs) { return (T) (static_cast<std::underlying_type_t <T>>(lhs) X static_cast<std::underlying_type_t <T>>(rhs)); } 
#define ENUM_FLAGS(T) \
enum class T; \
inline T operator ~ (T t) { return (T) (~static_cast<std::underlying_type_t <T>>(t)); } \
ENUM_FLAG_OPERATOR(T,|) \
ENUM_FLAG_OPERATOR(T,^) \
ENUM_FLAG_OPERATOR(T,&) \
enum class T

できるだけシンプルに使用する

ENUM_FLAGS(Fish)
{
    OneFish,
    TwoFish,
    RedFish,
    BlueFish
};

そして、彼らが言うように、証拠はプリンにあります:

ENUM_FLAGS(Hands)
{
    NoHands = 0,
    OneHand = 1 << 0,
    TwoHands = 1 << 1,
    LeftHand = 1 << 2,
    RightHand = 1 << 3
};

Hands hands = Hands::OneHand | Hands::TwoHands;
if ( ( (hands & ~Hands::OneHand) ^ (Hands::TwoHands) ) == Hands::NoHands)
{
    std::cout << "Look ma, no hands!" << std::endl;
}

個々の演算子を自由に定義解除できますが、私の非常に偏った意見では、C / C ++は低レベルの概念とストリームとのインターフェースであり、これらのビット演算子を冷たくて死んだ手からこじ開けることができますそして、私はそれらを維持するために私が思いつくことができるすべての不浄なマクロとビット反転呪文であなたと戦います。


2
マクロをそれほど嫌うなら、適切な C ++構成体を使用して、マクロの代わりにいくつかのテンプレート演算子を書いてみませんか?間違いなく、テンプレートアプローチの方が優れています。これは、std::enable_ifwith std::is_enumを使用して、無料の演算子のオーバーロードを列挙型のみで動作するように制限できるためです。またstd::underlying_type、強い型付けを失うことなくギャップを埋めるために、比較演算子(を使用)と論理演算子not を追加しました。一致しないのは、boolへの暗黙の変換だけですがflags != 0、それで!flags十分です。
monkey0506

1

通常、1ビットのセットのバイナリ番号に対応する整数値のセットを定義し、それらを加算します。これは、Cプログラマーが通常行う方法です。

だから(ビットシフト演算子を使用して値を設定します。たとえば、1 << 2はバイナリ100と同じです)

#define ENUM_1 1
#define ENUM_2 1 << 1
#define ENUM_3 1 << 2

C ++ではより多くのオプションがあり、intではなく新しい型を定義し(typedefを使用)、上記と同様に値を設定します。または、ビットフィールドまたはboolsのベクトルを定義します。最後の2つはスペース効率が非常に高く、フラグを処理するためにより多くの意味を持ちます。ビットフィールドには、型チェック(したがって、インテリセンス)を提供するという利点があります。

C ++プログラマーはあなたの問題にビットフィールドを使用すべきだと(明らかに主観的に)言いますが、Cプログラムで#defineアプローチがC ++プログラムで多く使用されているのをよく見かけます。

ビットフィールドはC#の列挙型に最も近いため、C#が列挙型をオーバーロードしてビットフィールド型にしようとしたのは奇妙です。列挙型は実際には「単一選択」型でなければなりません。


11
このように、C ++でのマクロを使用することは悪いです
BЈовић

3
C ++ 14では、バイナリリテラル(例:)を定義できる0b0100ため、1 << n形式は廃止されました。
ロブK

多分あなたはビットフィールドの代わりにビットセットを意味しました。
ホルヘベロン

1

以下の列挙フラグの短い例は、C#によく似ています。

私の意見では、このアプローチについては、コードが少なく、バグが少なく、コードが優れています。

#indlude "enum_flags.h"

ENUM_FLAGS(foo_t)
enum class foo_t
    {
     none           = 0x00
    ,a              = 0x01
    ,b              = 0x02
    };

ENUM_FLAGS(foo2_t)
enum class foo2_t
    {
     none           = 0x00
    ,d              = 0x01
    ,e              = 0x02
    };  

int _tmain(int argc, _TCHAR* argv[])
    {
    if(flags(foo_t::a & foo_t::b)) {};
    // if(flags(foo2_t::d & foo_t::b)) {};  // Type safety test - won't compile if uncomment
    };

ENUM_FLAGS(T)は、enum_flags.hで定義されているマクロです(100行未満、制限なしで自由に使用できます)。


1
ファイルenum_flags.hは、質問の第1リビジョンと同じですか?はいの場合、リビジョンURLを使用して参照できます:http
gnat

+1はきれいに見えます。SDKプロジェクトでこれを試してみます。
ガレットクラボーン14

1
@GaretClabornこれは私がきれいと呼ぶものです:paste.ubuntu.com/23883996
sehe

1
もちろん、::typeそこを見逃した。修正:paste.ubuntu.com/23884820
sehe

@seheちょっと、テンプレートコードは読みやすく、意味をなさないはずです。この魔術は何ですか?nice ....このスニペットはlolを使用できるようになっていますか
Garet Claborn

0

猫の皮を剥ぐ別の方法があります:

ビット演算子をオーバーロードする代わりに、少なくとも一部は4ライナーを追加してスコープ付き列挙の厄介な制限を回避することを好むかもしれません:

#include <cstdio>
#include <cstdint>
#include <type_traits>

enum class Foo : uint16_t { A = 0, B = 1, C = 2 };

// ut_cast() casts the enum to its underlying type.
template <typename T>
inline auto ut_cast(T x) -> std::enable_if_t<std::is_enum_v<T>,std::underlying_type_t<T>>
{
    return static_cast<std::underlying_type_t<T> >(x);
}

int main(int argc, const char*argv[])
{
   Foo foo{static_cast<Foo>(ut_cast(Foo::B) | ut_cast(Foo::C))};
   Foo x{ Foo::C };
   if(0 != (ut_cast(x) & ut_cast(foo)) )
       puts("works!");
    else 
        puts("DID NOT WORK - ARGHH");
   return 0;
}

確かに、ut_cast()毎回物をタイプする必要がありますがstatic_cast<>()、暗黙の型変換やoperator uint16_t()物の種類と比較して、使用するのと同じ意味で、より良いコードが得られます。

正直なところ、Foo上記のコードのようにtype を使用すると危険があります。

他のどこかで誰かが変数fooを切り替えて、複数の値を保持することを期待しないかもしれません...

そのため、コードをut_cast()散らかすことは、何か怪しいことが起こっていることを読者に警告するのに役立ちます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.