Verwenden von Bereichsaufzählungen für Bitflags in C ++

60

Ein enum X : int(C #) oder enum class X : int(C ++ 11) ist ein Typ, dessen verstecktes inneres Feld inteinen beliebigen Wert enthalten kann. Außerdem sind eine Reihe vordefinierter Konstanten von Xin der Enumeration definiert. Es ist möglich, die Enumeration auf ihren ganzzahligen Wert zu setzen und umgekehrt. Dies gilt sowohl für C # als auch für C ++ 11.

In C # werden Aufzählungen nicht nur zum Speichern einzelner Werte verwendet, sondern auch zum Speichern bitweiser Kombinationen von Flags, wie von Microsoft empfohlen . Solche Aufzählungen sind (normalerweise, aber nicht unbedingt) mit dem [Flags]Attribut versehen. Um das Leben der Entwickler zu vereinfachen, sind die bitweisen Operatoren (OR, AND, etc ...) überladen, sodass Sie auf einfache Weise Folgendes tun können (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Ich bin ein erfahrener C # -Entwickler, programmiere aber erst seit ein paar Tagen C ++ und bin mit den C ++ - Konventionen nicht vertraut. Ich beabsichtige, eine C ++ 11-Enumeration genauso zu verwenden, wie ich es in C # getan habe. In C ++ 11 sind die bitweisen Operatoren für bereichsspezifische Enums nicht überladen, daher wollte ich sie überladen .

Dies löste eine Debatte aus, und die Meinungen scheinen zwischen drei Optionen zu variieren:

  1. Eine Variable vom Typ enum wird verwendet, um das Bitfeld zu halten, ähnlich wie in 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)));

    Dies würde jedoch der stark typisierten Enum-Philosophie von C ++ 11 widersprechen.

  2. Verwenden Sie eine einfache Ganzzahl, wenn Sie eine bitweise Kombination von Enums speichern möchten:

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

    Dies würde jedoch alles auf eine reduzieren int, sodass Sie keine Ahnung haben, welchen Typ Sie in die Methode einfügen sollen.

  3. Schreiben Sie eine separate Klasse, die Operatoren überlädt und die bitweisen Flags in einem versteckten Ganzzahlfeld enthält:

    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);

    ( vollständiger Code von user315052 )

    Dann haben Sie jedoch weder IntelliSense noch eine andere Unterstützung, um auf die möglichen Werte hinzuweisen.

Ich weiß, dass dies eine subjektive Frage ist , aber: Welchen Ansatz soll ich verwenden? Welcher Ansatz ist in C ++ am weitesten verbreitet? Welchen Ansatz verwenden Sie beim Umgang mit Bitfeldern und warum ?

Da alle drei Ansätze funktionieren, suche ich natürlich nach sachlichen und technischen Gründen, allgemein anerkannten Konventionen und nicht nur nach persönlichen Vorlieben.

Aufgrund meines C # -Hintergrunds tendiere ich beispielsweise dazu, Ansatz 1 in C ++ zu wählen. Dies hat den zusätzlichen Vorteil, dass meine Entwicklungsumgebung mich auf die möglichen Werte hinweisen kann, und bei überladenen Enum-Operatoren ist dies einfach zu schreiben und zu verstehen und ziemlich sauber. Und die Methodensignatur zeigt deutlich, welchen Wert sie erwartet. Aber die meisten Leute hier stimmen mir nicht zu, wahrscheinlich aus gutem Grund.

Daniel AA Pelsmaeker
quelle
2
Das ISO C ++ - Komitee befand Option 1 für wichtig genug, um explizit anzugeben, dass der Wertebereich von Aufzählungen alle binären Kombinationen von Flags enthält. (Dies ist älter als C ++ 03) Es gibt also eine objektive Bestätigung dieser etwas subjektiven Frage.
MSalters
1
(Um den Kommentar von @MSalters zu verdeutlichen, basiert der Bereich einer C ++ - Aufzählung auf ihrem zugrunde liegenden Typ (wenn es sich um einen festen Typ handelt) oder auf ihren Enumeratoren. In letzterem Fall basiert der Bereich auf dem kleinsten Bitfeld, das alle definierten Enumeratoren enthalten kann ; zB für enum E { A = 1, B = 2, C = 4, };, der Bereich ist 0..7(3 Bit). Somit garantiert der C ++ - Standard explizit, dass # 1 immer eine praktikable Option ist. [Standardmäßig wird enum class, enum class : intsofern nicht anders angegeben, immer ein fester zugrunde liegender Typ verwendet.])
Justin Time

Antworten:

31

Am einfachsten ist es, den Bediener selbst überladen zu lassen. Ich denke an die Erstellung eines Makros, um die grundlegenden Überladungen pro Typ zu erweitern.

#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;
}

(Beachten Sie, dass type_traitses sich um einen C ++ 11-Header und std::underlying_type_teine C ++ 14-Funktion handelt.)

Dave
quelle
6
std :: zugrundeliegender_Typ_t ist C ++ 14. Kann in C ++ 11 den Typ std :: zugrundeliegender_Typ <T> :: verwenden.
ddevienne
14
Warum verwenden Sie static_cast<T>für die Eingabe, aber das Ergebnis wird hier im C-Stil umgewandelt?
Ruslan
2
@ Ruslan Ich zweite diese Frage
audiFanatic
Warum beschäftigen Sie sich überhaupt mit std :: underlying_type_t, wenn Sie bereits wissen, dass es sich um int handelt?
Poizan42
1
Wenn SBJFrameDragin einer Klasse definiert ist und der |Operator später in den Definitionen derselben Klasse verwendet wird, wie würden Sie den Operator so definieren, dass er in der Klasse verwendet werden kann?
HelloGoodbye
6

In der Vergangenheit hätte ich immer die alte (schwach typisierte) Aufzählung verwendet, um die Bitkonstanten zu benennen, und nur die Speicherklasse explizit verwendet, um das resultierende Flag zu speichern. Hier müsste ich sicherstellen, dass meine Aufzählungen in den Speichertyp passen, und die Zuordnung zwischen dem Feld und den zugehörigen Konstanten verfolgen.

Ich mag die Idee stark typisierter Aufzählungen, aber ich bin nicht wirklich mit der Idee einverstanden, dass Variablen des Aufzählungstyps Werte enthalten können, die nicht zu den Konstanten dieser Aufzählung gehören.

ZB unter der Annahme, das bitweise oder wurde überlastet:

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

Für Ihre dritte Option benötigen Sie ein Boilerplate, um den Speichertyp der Aufzählung zu extrahieren. Angenommen, wir möchten einen nicht signierten zugrunde liegenden Typ erzwingen (mit etwas mehr Code können wir auch signierte Typen verarbeiten):

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;

Dadurch erhalten Sie immer noch kein IntelliSense oder keine automatische Vervollständigung, aber die Erkennung des Speichertyps ist weniger hässlich als ursprünglich erwartet.


Jetzt habe ich eine Alternative gefunden: Sie können den Speichertyp für eine schwach typisierte Aufzählung angeben. Es hat sogar die gleiche Syntax wie in C #

enum E4 : int { ... };

Da es schwach typisiert ist und implizit in / von int konvertiert wird (oder welchen Speichertyp Sie auch immer wählen), ist es weniger seltsam, Werte zu haben, die nicht mit den aufgezählten Konstanten übereinstimmen.

Der Nachteil ist, dass dies als "Übergang" beschrieben wird ...

NB. Diese Variante fügt ihre aufgezählten Konstanten sowohl dem verschachtelten als auch dem umschließenden Bereich hinzu. Sie können dies jedoch mit einem Namespace umgehen:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
Nutzlos
quelle
1
Ein weiterer Nachteil von schwach typisierten Aufzählungen ist, dass ihre Konstanten meinen Namespace verschmutzen, da ihnen der Aufzählungsname nicht vorangestellt werden muss. Und das kann auch zu seltsamem Verhalten führen, wenn Sie zwei verschiedene Aufzählungen für ein Mitglied mit demselben Namen haben.
Daniel AA Pelsmaeker
Das ist richtig. Die schwach typisierte Variante mit dem angegebenen Speichertyp fügt ihre Konstanten sowohl dem einschließenden Bereich als auch seinem eigenen Bereich hinzu, iiuc.
Nutzlos
Der Enumerator ohne Gültigkeitsbereich wird nur im umgebenden Bereich deklariert. Die Qualifizierung anhand des Aufzählungsnamens ist Teil der Suchregeln und nicht der Deklaration. C ++ 11 7.2 / 10: Jeder Aufzählungsname und jeder Enumerator ohne Gültigkeitsbereich wird in dem Gültigkeitsbereich deklariert, der unmittelbar den Aufzählungsspezifizierer enthält. Jeder Bereichsaufzähler wird im Bereich der Aufzählung deklariert. Diese Namen entsprechen den in (3.3) und (3.4) für alle Namen definierten Gültigkeitsbereichsregeln.
Lars Viklund
1
Mit C ++ 11 haben wir std :: zugrundeliegender_Typ, der den zugrundeliegenden Typ einer Aufzählung liefert. Also haben wir 'template <typename IntegralType> struct Integral {typedef typename std :: zugrundeliegender_typ <IntegralType> :: type Type; }; `In C ++ 14 ist dies noch einfacher, um <typename IntegralType> struct Integral {typedef std :: underlying_type_t <IntegralType> Type; };
EMSR
4

Sie können typsichere Enum-Flags in C ++ 11 definieren, indem Sie verwenden std::enable_if. Dies ist eine rudimentäre Implementierung, bei der möglicherweise einige Dinge fehlen:

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;
}

Beachten Sie, number_of_bitsdass dies vom Compiler leider nicht ausgefüllt werden kann, da C ++ keine Möglichkeit hat, die möglichen Werte einer Aufzählung zu überprüfen.

Edit: Eigentlich stehe ich korrigiert da, es ist möglich den Compiler number_of_bitsfür dich auszufüllen .

Beachten Sie, dass dies einen nicht kontinuierlichen Enum-Wertebereich (ineffizient) verarbeiten kann. Sagen wir einfach, es ist keine gute Idee, das Obige mit einer Aufzählung wie dieser zu verwenden, da sonst der Wahnsinn entsteht:

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

Alles in allem ist dies jedoch eine durchaus brauchbare Lösung. Benötigt kein benutzerseitiges Bitfiddling, ist typsicher und innerhalb seiner Grenzen, so effizient wie es nur geht (ich verlasse mich hier stark auf die std::bitsetImplementierungsqualität ;)).

Rubenvb
quelle
Ich bin sicher, ich habe einige Überladungen der Operatoren verpasst.
Rubenvb
2

ich Hass Ich verabscheue Makros in meinem C ++ 14 genauso sehr wie den nächsten, aber ich habe mich daran gemacht, sie überall zu verwenden, und das auch recht großzügig:

#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

Gebrauch so einfach machen wie

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

Und wie sie sagen, ist der Beweis im Pudding:

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;
}

Sie können die Definition einzelner Operatoren jederzeit aufheben. Meiner Meinung nach eignet sich C / C ++ jedoch für die Verknüpfung mit Konzepten und Streams auf niedriger Ebene, und Sie können diese bitweisen Operatoren aus meinen kalten, toten Händen hebeln und ich werde dich mit all den unheiligen Makros und Sprüchen bekämpfen, die ich beschwören kann, um sie zu behalten.

Mahmoud Al-Qudsi
quelle
2
Wenn Sie Makros so sehr verabscheuen, warum nicht ein richtiges C ++ - Konstrukt verwenden und statt der Makros einige Template-Operatoren schreiben? Wahrscheinlich ist der Template-Ansatz besser, da Sie std::enable_ifmit std::is_enumIhre freien Operatorüberladungen darauf beschränken können, nur mit Aufzählungstypen zu arbeiten. Ich habe auch Vergleichsoperatoren (using std::underlying_type) und den logischen Nicht-Operator hinzugefügt , um die Lücke weiter zu schließen, ohne die starke Typisierung zu verlieren. Das einzige , was ich nicht ist implizite Konvertierung in bool mithalten kann, aber flags != 0und !flagsist für mich ausreichend.
monkey0506
1

Normalerweise definieren Sie eine Reihe von Ganzzahlwerten, die den gesetzten Einzelbit-Binärzahlen entsprechen, und addieren sie dann. Dies ist die Art und Weise, wie C-Programmierer dies normalerweise tun.

Also müssten Sie (mit dem Bitverschiebungsoperator die Werte einstellen, z. B. 1 << 2 ist das Gleiche wie Binär 100)

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

usw

In C ++ haben Sie mehr Optionen. Definieren Sie einen neuen Typ, der ein int ist (verwenden Sie typedef ), und setzen Sie die Werte wie oben beschrieben. oder definieren Sie ein Bitfeld oder einen Vektor von Bools . Die letzten beiden sind sehr platzsparend und für den Umgang mit Flags viel sinnvoller. Ein Bitfeld hat den Vorteil, dass Sie die Typüberprüfung (und damit die Intellisense-Funktion) durchführen können.

Ich würde sagen (natürlich subjektiv), dass ein C ++ - Programmierer ein Bitfeld für Ihr Problem verwenden sollte, aber ich neige dazu, den von C-Programmen häufig in C ++ -Programmen verwendeten #define-Ansatz zu sehen.

Ich nehme an, das Bitfeld ist der Aufzählung von C # am nächsten. Deshalb ist es seltsam, dass C # versucht hat, eine Aufzählung als Bitfeldtyp zu überladen - eine Aufzählung sollte eigentlich ein "Single-Select" -Typ sein.

gbjbaanb
quelle
11
Die Verwendung von Makros in c ++ auf diese Weise ist schlecht
BЈови13
3
In C ++ 14 können Sie Binärliterale definieren (z. B. 0b0100), sodass das 1 << nFormat veraltet ist.
Rob K.,
Vielleicht hast du Bitset statt Bitfield gemeint.
Jorge Bellon
1

Ein kurzes Beispiel für Enum-Flags unten ähnelt C #.

Über den Ansatz, meiner Meinung nach: weniger Code, weniger Bugs, besserer Code.

#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) ist ein in enum_flags.h definiertes Makro (weniger als 100 Zeilen, frei und ohne Einschränkungen verwendbar).

Yuri Yaryshev
quelle
1
Entspricht die Datei enum_flags.h der ersten Überarbeitung Ihrer Frage? Wenn ja, können Sie die URL der Revision verwenden, um darauf zu verweisen: http://programmers.stackexchange.com/revisions/205567/1
gnat
+1 sieht gut aus, sauber. Ich werde dies in unserem SDK-Projekt ausprobieren.
Garet Claborn
1
@GaretClaborn Das würde ich als sauber bezeichnen: paste.ubuntu.com/23883996
sehe
1
Natürlich hat das ::typedort gefehlt . Behoben
sehe
@sehe hey, Template-Code soll nicht lesbar und sinnvoll sein. Was ist das für eine Hexerei? schön .... ist dieser schnipsel offen um lol zu benutzen
garet claborn
0

Es gibt noch einen anderen Weg, die Katze zu häuten:

Anstatt die Bit-Operatoren zu überladen, ziehen es einige vielleicht vor, nur einen 4-Liner hinzuzufügen, um diese unangenehme Einschränkung von Aufzählungen zu umgehen:

#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;
}

Zugegeben, Sie müssen das ut_cast()Ding jedes Mal tippen , aber auf der anderen Seite liefert dies besser lesbaren Code im gleichen Sinne wie die Verwendung static_cast<>()im Vergleich zu impliziter Typkonvertierung oder operator uint16_t()Art von Dingen.

Und seien wir ehrlich, die Verwendung von Typ Foowie im obigen Code birgt seine Gefahren:

An einer anderen Stelle könnte jemand eine Switch-Case-Operation über eine Variable ausführen foound nicht erwarten, dass sie mehr als einen Wert enthält ...

Wenn Sie also den Code damit ut_cast()verunreinigen, werden Sie darauf aufmerksam gemacht, dass etwas faul ist.

BitTickler
quelle