it-swarm.com.de

Verwenden von Enums mit Gültigkeitsbereich für Bitflags in C ++

Ein enum X : int (C #) oder enum class X : int (C++ 11) ist ein Typ mit einem versteckten inneren Feld von int, das einen beliebigen Wert enthalten kann. Zusätzlich wird eine Reihe vordefinierter Konstanten von X in der Aufzählung definiert. Es ist möglich, die Aufzählung auf ihren ganzzahligen Wert zu setzen und umgekehrt. Dies gilt sowohl für C # als auch für C++ 11.

In C # werden Enums nicht nur verwendet, um einzelne Werte zu speichern, sondern auch, um bitweise Kombinationen von Flags gemäß Empfehlung von Microsoft zu speichern. Solche Aufzählungen sind (normalerweise, aber nicht unbedingt) mit dem [Flags] Attribut. Um das Leben der Entwickler zu vereinfachen, sind die bitweisen Operatoren (OR, AND usw.) ü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 in Enums mit Gültigkeitsbereich nicht überladen, also ich wollte 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 bei 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 Aufzählungsphilosophie der C++ 11-Aufzählungen widersprechen.

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

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

    Dies würde jedoch alles auf ein int reduzieren und Ihnen keine Ahnung geben, welchen Typ Sie in die Methode einfügen sollen.

  3. Schreiben Sie eine separate Klasse, die Operatoren überlastet und die bitweisen Flags in einem versteckten Ganzzahlfeld hä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 ser315052 )

    Aber dann haben Sie kein IntelliSense oder irgendeine Unterstützung, die Sie auf die möglichen Werte hinweist.

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.

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

Am einfachsten ist es, den Bediener selbst zu überlasten. Ich denke darüber nach, ein Makro zu erstellen, 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_traits ist ein C++ 11-Header und std::underlying_type_t ist eine C++ 14-Funktion.)

32
Dave

Sie können typsichere Enum-Flags in C++ 11 mit std::enable_if Definieren. 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() 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;
}

Beachten Sie, dass number_of_bits 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, es ist möglich, den Compiler für dich number_of_bits Füllen zu lassen.

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

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

Aber alles in allem ist dies am Ende eine durchaus brauchbare Lösung. Benötigt kein benutzerseitiges Bitfiddling, ist typsicher und innerhalb seiner Grenzen so effizient wie es nur geht (ich stütze mich hier stark auf die Implementierungsqualität von std::bitset;)).

6
rubenvb

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 wäre es meine Aufgabe, sicherzustellen, dass meine Aufzählungen in den Speichertyp passen, und die Zuordnung zwischen dem Feld und den zugehörigen Konstanten zu verfolgen.

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

ZB unter der Annahme, dass das bitweise oder überladen wurde:

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 eine Boilerplate, um den Speichertyp der Aufzählung zu extrahieren. Angenommen, wir möchten einen nicht signierten zugrunde liegenden Typ erzwingen (wir können auch signierte mit etwas mehr Code 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;

Dies gibt Ihnen 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 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
6
Useless

Ich hass verabscheue Makros in meinem C++ 14 genauso wie den nächsten, aber ich habe es mir zur Aufgabe gemacht, diese überall und auch ganz großzügig zu verwenden:

#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

So einfach wie möglich nutzen

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 jeden einzelnen Operator nach Belieben aufheben, aber meiner Meinung nach ist C/C++ für die Anbindung an Konzepte und Streams auf niedriger Ebene gedacht, und Sie können diese bitweisen Operatoren aus meinen kalten, toten Händen herausholen und ich werde dich mit all den unheiligen Makros und Zaubersprüchen bekämpfen, die ich heraufbeschwören kann, um sie zu behalten.

3

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

Das hätten Sie also (mit dem Bitverschiebungsoperator können Sie die Werte festlegen, z. B. 1 << 2 entspricht 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 legen Sie die Werte auf ähnliche Weise wie oben fest. oder definieren Sie ein Bitfeld oder ein 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 eine Typprüfung (und damit Intellisense) durchführen können.

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

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

1
gbjbaanb

Ein kurzes Beispiel für Enum-Flags unten sieht C # ziemlich ähnlich.

Über den Ansatz, meiner Meinung nach: weniger Code, weniger Fehler, 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 Makro, definiert in enum_flags.h (weniger als 100 Zeilen, frei zu verwenden ohne Einschränkungen).

1
Yuri Yaryshev

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

Anstatt die Bitoperatoren zu überladen, ziehen es zumindest einige vor, nur einen 4-Liner hinzuzufügen, um diese unangenehme Einschränkung von Enums mit Gültigkeitsbereich 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 jedes Mal das Ding ut_cast() eingeben, aber auf der anderen Seite ergibt dies im Vergleich zur impliziten Typkonvertierung mehr lesbaren Code im gleichen Sinne wie bei Verwendung von static_cast<>() oder operator uint16_t() Art von Dingen.

Und seien wir hier ehrlich: Die Verwendung des Typs Foo wie im obigen Code birgt Gefahren:

An einem anderen Ort könnte jemand einen Switch-Fall über die Variable foo ausführen und nicht erwarten, dass sie mehr als einen Wert enthält ...

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

0
BitTickler