web-dev-qa-db-ja.com

C ++でのビットフラグのスコープ列挙型の使用

enum X : int(C#)またはenum class X : int(C++ 11)は、任意の値を保持できるintの非表示の内部フィールドを持つ型です。さらに、Xのいくつかの事前定義された定数が列挙型で定義されています。 enumを整数値にキャストしたり、その逆を行うことができます。これは、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. Enum型の変数は、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);
    

    完全なコードser315052 による)

    ただし、IntelliSenseや、考えられる値を示すためのサポートはありません。

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

もちろん、3つのアプローチすべてが機能するので、私は事実上のおよび技術的な理由、一般的に受け入れられている規則を探しています。

たとえば、C#の背景のため、C++ではアプローチ1を採用する傾向があります。これには、開発環境が可能な値についてヒントを与えることができるという追加の利点があります。列挙型演算子がオーバーロードされているため、これは簡単に記述および理解でき、非常にクリーンです。そして、メソッドシグネチャは、それが期待する値の種類を明確に示します。しかし、ほとんどの人 here おそらく私には同意しません。

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

#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の機能です。)

32
Dave

std::enable_ifを使用すると、C++ 11でタイプセーフな列挙型フラグを定義できます。これは、いくつか欠落している可能性がある基本的な実装です。

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() const { 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;
}

C++には列挙の可能な値をイントロスペクトする方法がないため、残念ながらnumber_of_bitsはコンパイラーで埋めることができません。

編集:実際には私は修正されたままですが、コンパイラーにnumber_of_bitsを埋め込むことができます。

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

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

しかし、これを考慮したすべてのものは、最終的には非常に使いやすいソリューションです。ユーザーサイドのビットフィドリングは必要なく、タイプセーフであり、その範囲内で、可能な限り効率的です(私はstd::bitset実装品質;)に強く依存しています)。

6
rubenvb

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

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

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

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番目のオプションでは、列挙型のストレージタイプを抽出するためのボイラープレートが必要です。署名されていない基本型を強制したいと仮定します(もう少しコードを使用して、signedも処理できます)。

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
6
Useless

私 嫌い 私の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++は低レベルの概念とストリームとのインターフェースであり、これらのビット単位の演算子を私の冷たく死んだ手から引き上げることができます。そして、私はそれらを維持するために想起させることができるすべての非聖なるマクロとビットフリッピング呪文であなたと戦うでしょう。

3

通常は、シングルビットセットの2進数に対応する整数値のセットを定義し、それらを加算します。これは、Cプログラマが通常行う方法です。

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

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

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

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

私はビットフィールドがC#の列挙型に最も近いと思います。C#が列挙型をオーバーロードしてビットフィールド型にしようとしたのは奇妙です-列挙型は本当に「単一選択」型である必要があります。

1
gbjbaanb

以下の列挙フラグの短い例は、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
Yuri Yaryshev

猫の皮をむく方法は他にもあります:

ビット演算子をオーバーロードする代わりに、少なくとも一部は、スコープ付き列挙型の厄介な制限を回避するために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を使用すると危険が伴います。

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

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

0
BitTickler