it-swarm.com.de

Auf der Suche nach einer besseren Bitflag-Aufzählung

Okay, wir sind bei C++ 17 und es gibt noch keine zufriedenstellende Antwort auf eine wirklich gute Bitflags-Schnittstelle in C++.

Wir haben enum, die ihre Member-Werte in den umschließenden Bereich einschließen, aber implizit in ihren zugrunde liegenden Typ konvertieren. Sie können also so verwendet werden, als wären sie Bit-Flags, lehnen jedoch eine Neuzuweisung in die Enumeration ab, ohne sie erneut zu gießen.

Wir haben enum class, der das Problem mit dem Namensumfang löst, sodass deren Werte explizit als MyEnum::MyFlag oder sogar als MyClass::MyEnum::MyFlag bezeichnet werden müssen. Sie werden jedoch nicht implizit in den zugrunde liegenden Typ konvertiert und können daher nicht als Bit-Flags ohne endloses Hin- und Herschalten verwendet werden.

Und schließlich haben wir die alten Bitfelder von C wie:

struct FileFlags {
   unsigned ReadOnly : 1;
   unsigned Hidden : 1;
   ...
};

Was den Nachteil hat, dass es keine gute Möglichkeit gibt, sich als Ganzes zu initialisieren - man muss auf memset zurückgreifen oder die Adresse oder Ähnliches umwandeln, um den gesamten Wert zu überschreiben oder alles auf einmal zu initialisieren oder auf andere Weise mehrere Bits auf einmal zu manipulieren. Es leidet auch daran, dass es nicht möglich ist, den Wert eines bestimmten Flags im Gegensatz zu seiner Adresse zu benennen - es gibt also keinen Namen, der 0x02 darstellt, wohingegen es einen solchen Namen gibt, wenn Enums verwendet werden. Daher ist es mit Enums einfach, eine Kombination von zu benennen Flags wie FileFlags::ReadOnly | FileFlags::Hidden- für Bitfelder gibt es einfach keine gute Möglichkeit, so viel zu sagen.

Außerdem haben wir noch einfache constexpr oder #define, um Bitwerte zu benennen und dann einfach überhaupt keine Aufzählungen zu verwenden. Dies funktioniert, trennt jedoch die Bitwerte vollständig vom zugrunde liegenden Bitflag-Typ. Vielleicht ist dies letztendlich nicht der schlechteste Ansatz, besonders wenn die Bitflag-Werte constexpr in einer Struktur sind, um ihnen ihren eigenen Namensumfang zu geben?

struct FileFlags {
    constexpr static uint16_t ReadOnly = 0x01u;
    constexpr static uint16_t Hidden = 0x02u;
    ...
}

So wie es derzeit aussieht, haben wir eine Menge Techniken, von denen keine eine wirklich solide Art zu sagen ergibt

Hier ist ein Typ, der die folgenden gültigen Bit-Flags enthält, einen eigenen Namensumfang und diese Bits und Typen sollten mit standardmäßigen bitweisen Operatoren wie | frei verwendbar sein & ^ ~, und sie sollten mit Integralwerten wie 0 vergleichbar sein, und das Ergebnis von bitweisen Operatoren sollte der angegebene Typ bleiben und sich nicht in ein Integral verwandeln

Trotzdem gibt es eine Reihe von Versuchen, die obige Entität in C++ - zu erzeugen.

  1. Das Windows-OS-Team hat ein einfaches Makro entwickelt, das C++ - Code generiert, um die erforderlichen fehlenden Operatoren für einen bestimmten Aufzählungstyp zu definieren. DEFINE_ENUM_FLAG_OPERATORS(EnumType) definiert dann operator | & ^ ~ und die zugehörigen Zuweisungsoperationen wie | = und usw.
  2. 'grisumbras' hat ein öffentliches GIT-Projekt, um Bitflag-Semantik mit Bereichs-Aufzählungen zu ermöglichen hier , das enable_if-Metaprogrammierung verwendet, damit eine gegebene Aufzählung in einen Bit-Flag-Typ konvertiert werden kann, der die fehlenden Operatoren unterstützt und im Hintergrund zurückkehrt.
  3. Ohne das oben Gesagte zu kennen, habe ich einen relativ einfachen bit_flags-Wrapper geschrieben, der alle bitweisen Operatoren für sich definiert, sodass man einen bit_flags<EnumType> flags verwenden kann und dann flags eine bitweise Semantik hat. Dies führt nicht dazu, dass die aufgezählte Basis bitweise Operatoren direkt richtig handhaben kann. Sie können also EnumType::ReadOnly | EnumType::Hidden nicht sagen, auch wenn Sie einen bit_flags<EnumType> verwenden, da die zugrunde liegende Aufzählung selbst die erforderlichen Operatoren immer noch nicht unterstützt. Am Ende musste ich im Wesentlichen das Gleiche tun wie oben bei # 1 und # 2 und operator | (EnumType, EnumType) für die verschiedenen bitweisen Operatoren aktivieren, indem die Benutzer aufgefordert wurden, eine Spezialisierung für einen Metatyp für ihre Aufzählung wie template <> struct is_bitflag_enum<EnumType> : std::true_type {}; anzugeben.

Letztendlich besteht das Problem bei # 1, # 2 und # 3 darin, dass es meines Wissens nicht möglich ist, die fehlenden Operatoren in der Enumeration selbst zu definieren (wie in # 1) oder den erforderlichen Enabler-Typ zu definieren ( zB template <> struct is_bitflag_enum<EnumType> : std::true_type {}; wie in # 2 und teilweise # 3) im Klassenumfang. Diese müssen außerhalb einer Klasse oder Struktur auftreten, da C++ einfach keinen mir bekannten Mechanismus hat, mit dem ich solche Deklarationen innerhalb einer Klasse machen kann.

Jetzt möchte ich eine Reihe von Flags haben, die für eine bestimmte Klasse bestimmt sind, aber ich kann diese Flags nicht im Klassenheader verwenden (z. B. Standardinitialisierung, Inline-Funktionen usw.), da ich keine der folgenden Optionen aktivieren kann Maschinen, mit denen die Aufzählung bis nach der schließenden Klammer für die Klassendefinition als Bitflags behandelt werden kann. Oder ich kann alle diese Flag-Enums außerhalb der Klasse definieren, zu der sie gehören, sodass ich vor der Definition der Benutzerklasse die Option "Diese Enumeration in einen bitweisen Typ umwandeln" aufrufen kann, um diese Funktionalität in vollem Umfang nutzen zu können die Client-Klasse - aber jetzt sind die Bit-Flags im äußeren Bereich, anstatt der Klasse selbst zugeordnet zu sein.

Dies ist nicht das Ende der Welt - keine der oben genannten ist. Aber all das bereitet endlose Kopfschmerzen beim Schreiben meines Codes - und hindert mich daran, ihn auf natürlichste Weise zu schreiben - dh mit einem bestimmten Flag-Enum, das zu einer bestimmten Klasse in dieser Client-Klasse gehört (für diese Klasse gilt), aber mit bitweisem Flag -semantics (mein Ansatz # 3 erlaubt dies fast - solange alles von einem bit_flags umschlossen ist - um explizit die benötigte bitweise Kompatibilität zu ermöglichen).

All dies lässt mich immer noch mit dem ärgerlichen Gefühl zurück, dass dies viel besser sein könnte als es ist!

Sicherlich sollte es einen Ansatz für Enums geben - und vielleicht auch, aber ich habe es noch nicht herausgefunden -, um bitweise Operatoren für sie zu aktivieren, während sie deklariert und in einem umschließenden Klassenbereich verwendet werden können ...

Hat jemand einen Trick oder einen Ansatz, den ich oben nicht in Betracht gezogen habe, der mir erlauben würde, "das Beste von allen möglichen Welten" auf diesem Gebiet zu machen?

17
Mordachai

Zum Beispiel

// union only for convenient bit access. 
typedef union a
{ // it has its own name-scope
    struct b
     {
         unsigned b0 : 1;
         unsigned b2 : 1;
         unsigned b3 : 1;
         unsigned b4 : 1;
         unsigned b5 : 1;
         unsigned b6 : 1;
         unsigned b7 : 1;
         unsigned b8 : 1;
         //...
     } bits;
    unsigned u_bits;
    // has the following valid bit-flags in it
    typedef enum {
        Empty = 0u,
        ReadOnly = 0x01u,
        Hidden  = 0x02u
    } Values;
    Values operator =(Values _v) { u_bits = _v; return _v; }
     // should be freely usable with standard bitwise operators such as | & ^ ~   
    union a& operator |( Values _v) { u_bits |= _v; return *this; }
    union a& operator &( Values _v) { u_bits &= _v; return *this; }
    union a& operator |=( Values _v) { u_bits |= _v; return *this; }
    union a& operator &=( Values _v) { u_bits &= _v; return *this; }
     // ....
    // they should be comparable to integral values such as 0
    bool operator <( unsigned _v) { return u_bits < _v; }
    bool operator >( unsigned _v) { return u_bits > _v; }
    bool operator ==( unsigned _v) { return u_bits == _v; }
    bool operator !=( unsigned _v) { return u_bits != _v; }
} BITS;


int main()
 {
     BITS bits;
     int integral = 0;

     bits = bits.Empty;

     // they should be comparable to integral values such as 0
     if ( bits == 0)
     {
         bits = bits.Hidden;
         // should be freely usable with standard bitwise operators such as | & ^ ~
         bits = bits | bits.ReadOnly;
         bits |= bits.Hidden;
         // the result of any bitwise operators should remain the named type, and not devolve into an integral
         //bits = integral & bits; // error
         //bits |= integral; // error
     }
 }
1
Andrey Sv

Sie können Friend-Funktionen innerhalb einer einschließenden Klasse haben, die die Enumeration als Werte verwendet. Dies kann innerhalb eines Makros verwendet werden, um die erforderlichen Funktionen innerhalb eines Klassenbereichs zu definieren.

Zum Beispiel, um die is_bitflag_enum spezialisiereigenschaft, spezialisiere eine Struktur, die die Aufzählungen und die Operatoren enthält. Dies ist wie Nummer 2 und kann in einer Klasse immer noch nicht durchgeführt werden.

#include <type_traits>

template<class Tag>
struct bitflag {
    enum class type;

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr type operator OP(type lhs, type rhs) noexcept { \
        typedef typename ::std::underlying_type<type>::type underlying; \
        return static_cast<type>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr type& operator OP ## = (type& lhs, type rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(|)
    DEFINE_BITFLAG_OPERATOR(&)
    DEFINE_BITFLAG_OPERATOR(^)

#undef DEFINE_BITFLAG_OPERATOR

#define DEFINE_BITFLAG_OPERATOR(OP) \
    friend constexpr bool operator OP(type lhs, typename ::std::underlying_type<type>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<type>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<type>::type lhs, type rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<type>::type>(rhs); \
    }

    DEFINE_BITFLAG_OPERATOR(==)
    DEFINE_BITFLAG_OPERATOR(!=)
    DEFINE_BITFLAG_OPERATOR(<)
    DEFINE_BITFLAG_OPERATOR(>)
    DEFINE_BITFLAG_OPERATOR(>=)
    DEFINE_BITFLAG_OPERATOR(<=)

#undef DEFINE_BITFLAG_OPERATOR

    friend constexpr type operator~(type e) noexcept {
        return static_cast<type>(~static_cast<typename ::std::underlying_type<type>::type>(e));
    }

    friend constexpr bool operator!(type e) noexcept {
        return static_cast<bool>(static_cast<typename ::std::underlying_type<type>::type>(e));
    }
};

// The `struct file_flags_tag` (Which declares a new type) differentiates between different
// enum classes declared
template<> enum class bitflag<struct file_flags_tag>::type {
    none = 0,
    readable = 1 << 0,
    writable = 1 << 1,
    executable = 1 << 2,
    hidden = 1 << 3
};

using file_flags = bitflag<file_flags_tag>::type;

bool is_executable(file_flags f) {
    return (f & file_flags::executable) == 0;
}

Sie können auch ein einzelnes Makro erstellen, um jede einzelne Friend-Funktion zu definieren. Dies ist wie # 1, aber es liegt alles in einem Klassenbereich.

#include <type_traits>

#define MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(OP, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator OP(ENUM_TYPE lhs, ENUM_TYPE rhs) noexcept { \
        typedef typename ::std::underlying_type<ENUM_TYPE>::type underlying; \
        return static_cast<ENUM_TYPE>(static_cast<underlying>(lhs) OP static_cast<underlying>(rhs)); \
    } \
    friend constexpr ENUM_TYPE& operator OP ## = (ENUM_TYPE& lhs, ENUM_TYPE rhs) noexcept { \
        return (lhs = lhs OP rhs); \
    }

#define MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(OP, ENUM_TYPE) \
    friend constexpr bool operator OP(ENUM_TYPE lhs, typename ::std::underlying_type<ENUM_TYPE>::type rhs) noexcept { \
        return static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(lhs) OP rhs; \
    } \
    friend constexpr bool operator OP(typename ::std::underlying_type<ENUM_TYPE>::type lhs, ENUM_TYPE rhs) noexcept { \
        return lhs OP static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(rhs); \
    }


#define MAKE_BITFLAG_FRIEND_OPERATORS(ENUM_TYPE) \
    public: \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(|, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(&, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BITWISE(^, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(==, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(!=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(>=, ENUM_TYPE) \
    MAKE_BITFLAG_FRIEND_OPERATORS_BOOLEAN(<=, ENUM_TYPE) \
    friend constexpr ENUM_TYPE operator~(ENUM_TYPE e) noexcept { \
        return static_cast<ENUM_TYPE>(~static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    } \
    friend constexpr bool operator!(ENUM_TYPE e) noexcept { \
        return static_cast<bool>(static_cast<typename ::std::underlying_type<ENUM_TYPE>::type>(e)); \
    }

// ^ The above in a header somewhere

class my_class {
public:
    enum class my_flags {
        none = 0, flag_a = 1 << 0, flag_b = 1 << 2
    };

    MAKE_BITFLAG_FRIEND_OPERATORS(my_flags)

    bool has_flag_a(my_flags f) {
        return (f & my_flags::flag_a) == 0;
    }
};
1
Artyer

Ich nehme den Ansatz von Xaqqs FlagSet in Code Review SE .

Der Schlüssel besteht darin, einen neuen Typ einzuführen, der als "Container" für einen oder mehrere eingeschaltete Werte aus einer festgelegten Liste von Optionen dient. Dieser Container ist ein Wrapper für bitset, der als Eingabe Instanzen einer Bereichsaufzählung verwendet.

Es ist dank der Bereichsaufzählung typsicher und kann bitweise Operationen über eine Überladung des Operators ausführen, die an Bitgruppenoperationen delegiert wird. Sie können die Bereichsaufzählung auch dann direkt verwenden, wenn Sie dies wünschen und wenn Sie die bitweisen Operationen nicht benötigen oder wenn Sie mehrere Flags speichern möchten.

Für die Produktion habe ich einige Änderungen am verknüpften Code vorgenommen. Einige davon werden in den Kommentaren auf der Seite "Code Review" erläutert.

Ich verwende enum class mit den folgenden Vorlagenoperatoren:

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM operator |( ENUM lhs, ENUM rhs )
{
    return static_cast< ENUM >( static_cast< UInt32 >( lhs ) | static_cast< UInt32 >( rhs ));
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator |=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs | rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline UInt32 operator &( ENUM lhs, ENUM rhs )
{
    return static_cast< UInt32 >( lhs ) & static_cast< UInt32 >( rhs );
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs & rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, int rhs )
{
    lhs = static_cast< ENUM >( static_cast< int >( lhs ) & rhs );
    return lhs;
}

Wenn Sie Bedenken haben, dass die oben genannten Operatoren in andere Enums eindringen, können Sie sie vermutlich im selben Namespace einkapseln, in dem die Enumeration deklariert ist, oder sie einfach enumweise implementieren (ich habe früher ein Makro für verwendet) Das). Im Allgemeinen habe ich diesen Overkill jedoch in Betracht gezogen und sie jetzt in meinem Namespace der obersten Ebene deklariert, damit jeder Code verwendet werden kann.

0
James