Używanie wyliczeń zakresowych dla flag bitowych w C ++


60

enum X : int(C #) lub enum class X : int(C ++ 11) jest typem, który ma ukryte wewnętrzne pole int, które może pomieścić dowolną wartość. Ponadto Xw wyliczeniu zdefiniowano szereg predefiniowanych stałych . Możliwe jest rzutowanie wyliczenia na jego liczbę całkowitą i odwrotnie. Dotyczy to zarówno C #, jak i C ++ 11.

W języku C # wyliczenia są używane nie tylko do przechowywania pojedynczych wartości, ale także do przechowywania bitowych kombinacji flag, zgodnie z zaleceniami Microsoftu . Takie wyliczenia są (zwykle, ale niekoniecznie) ozdobione [Flags]atrybutem. Aby ułatwić życie programistom, operatory bitowe (LUB, AND itd.) Są przeciążone, dzięki czemu można łatwo zrobić coś takiego (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Jestem doświadczonym programistą C #, ale programuję C ++ dopiero od kilku dni i nie znam konwencji C ++. Zamierzam używać wyliczenia C ++ 11 dokładnie w taki sam sposób, jak to robiłem w C #. W C ++ 11 operatory bitowe w wyliczeniach zakresowych nie są przeciążone, więc chciałem je przeciążyć .

To wywołało debatę, a opinie wydają się różnić między trzema opcjami:

  1. Zmienna typu wyliczeniowego służy do przechowywania pola bitowego, podobnie do 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)));

    Byłoby to jednak sprzeczne z silnie wpisaną filozofią wyliczania enum w C ++ 11.

  2. Użyj zwykłej liczby całkowitej, jeśli chcesz przechowywać bitową kombinację wyliczeń:

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

    Ale to zredukowałoby wszystko do int, pozostawiając cię bez pojęcia, jaki typ powinieneś zastosować w metodzie.

  3. Napisz osobną klasę, która przeciąża operatorów i przechowuje flagi bitowe w ukrytym polu liczby całkowitej:

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

    ( pełny kod autorstwa użytkownika315052 )

    Ale wtedy nie masz IntelliSense ani żadnego wsparcia, które wskazywałoby na możliwe wartości.

Wiem, że to subiektywne pytanie , ale: Jakiego podejścia powinienem użyć? Jakie podejście jest najbardziej rozpowszechnione w C ++? Jakiego podejścia używasz w przypadku pól bitowych i dlaczego ?

Oczywiście, ponieważ wszystkie trzy podejścia działają, szukam przyczyn faktycznych i technicznych, ogólnie przyjętych konwencji, a nie tylko osobistych preferencji.

Na przykład, z powodu mojego tła w języku C #, zwykle używam podejścia 1 w C ++. Ma to tę dodatkową zaletę, że moje środowisko programistyczne może podpowiedzieć mi o możliwych wartościach, a przy przeciążonych operatorach enum jest to łatwe do napisania i zrozumienia oraz całkiem czyste. Podpis metody pokazuje wyraźnie, jakiej wartości oczekuje. Ale większość ludzi tutaj się ze mną nie zgadza, prawdopodobnie z ważnego powodu.


2
Komitet ISO C ++ uznał opcję 1 za wystarczająco ważną, aby wyraźnie stwierdzić, że zakres wartości wyliczeń obejmuje wszystkie binarne kombinacje flag. (To poprzedza C ++ 03) Więc istnieje obiektywna aprobata tego nieco subiektywnego pytania.
MSalters

1
(Aby wyjaśnić komentarz @MSalters, zakres wyliczenia C ++ opiera się na jego typie bazowym (jeśli jest to typ stały) lub w inny sposób na jego modułach wyliczających. W tym ostatnim przypadku zakres opiera się na najmniejszym polu bitowym, które może pomieścić wszystkie zdefiniowane moduły wyliczające ; np. dla enum E { A = 1, B = 2, C = 4, };zakresu jest 0..7(3 bity). W związku z tym standard C ++ wyraźnie gwarantuje, że nr 1 zawsze będzie realną opcją. [W szczególności enum classdomyślnie, enum class : intchyba że określono inaczej, a zatem zawsze ma ustalony typ bazowy.])
Justin Time

Odpowiedzi:


31

Najprostszym sposobem jest zapewnienie sobie przeciążenia operatora. Zastanawiam się nad utworzeniem makra w celu rozszerzenia podstawowych przeciążeń według typu.

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

(Uwaga: type_traitsjest to nagłówek C ++ 11 i std::underlying_type_tfunkcja C ++ 14).


6
std :: bazowy_typ_t to C ++ 14. Można użyć std :: bazowego typu <T> :: w C ++ 11.
ddevienne

14
Dlaczego używasz static_cast<T>do wprowadzania danych, a rzutowanie w stylu C dla wyników tutaj?
Ruslan

2
@ Ruslan I drugie pytanie
audiFanatic

Dlaczego w ogóle męczysz się ze std :: Underground_type_t, skoro już wiesz, że to int?
poizan42,

1
Jeśli SBJFrameDragjest zdefiniowany w klasie, a |-operator jest później używany w definicjach tej samej klasy, to jak zdefiniowałbyś operatora, aby mógł być użyty w klasie?
HelloGoodbye,

6

Historycznie zawsze używałbym starego (słabo wpisanego) wyliczenia do nazwania stałych bitowych, a po prostu jawnie używałbym klasy pamięci do przechowywania wynikowej flagi. Tutaj spoczywałby na mnie obowiązek upewnienia się, że moje wyliczenia pasują do typu pamięci i śledzenia powiązania między polem a powiązanymi stałymi.

Podoba mi się pomysł silnie typowanych wyliczeń, ale nie bardzo podoba mi się pomysł, że zmienne typu wyliczeniowego mogą zawierać wartości, które nie należą do stałych tego wyliczenia.

Np. Zakładając bitowe lub zostało przeciążone:

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

W przypadku trzeciej opcji potrzebujesz płyty kotłowej, aby wyodrębnić typ przechowywania wyliczenia. Zakładając, że chcemy wymusić niepodpisany typ bazowy (możemy również obsłużyć podpisany, z odrobiną kodu):

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;

To wciąż nie zapewnia IntelliSense ani autouzupełniania, ale wykrywanie typu pamięci jest mniej brzydkie, niż początkowo się spodziewałem.


Teraz znalazłem alternatywę: możesz określić typ magazynu dla słabo wpisanego wyliczenia. Ma nawet taką samą składnię jak w C #

enum E4 : int { ... };

Ponieważ jest słabo wpisany i niejawnie konwertuje do / z int (lub dowolnego innego rodzaju pamięci), mniej dziwnie jest mieć wartości, które nie pasują do wyliczonych stałych.

Minusem jest to, że jest to określane jako „przejściowe” ...

NB. ten wariant dodaje swoje wyliczone stałe zarówno do zagnieżdżonego, jak i obejmującego zakresu, ale można obejść to za pomocą przestrzeni nazw:

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

1
Innym minusem słabo wpisanych wyliczeń jest to, że ich stałe zanieczyszczają moją przestrzeń nazw, ponieważ nie muszą być poprzedzone nazwą wyliczenia. A to może również powodować wszelkiego rodzaju dziwne zachowania, jeśli masz dwa różne wyliczenia, oba z członkiem o tej samej nazwie.
Daniel AA Pelsmaeker,

To prawda. Słabo wpisany wariant z określonym typem pamięci dodaje swoje stałe zarówno do zakresu obejmującego, jak i do własnego zakresu, iiuc.
Bezużyteczne

Nieokreślony moduł wyliczający jest deklarowany tylko w otaczającym zakresie. Możliwość zakwalifikowania go według nazwy enum jest częścią reguł wyszukiwania, a nie deklaracji. C ++ 11 7.2 / 10: Każda nazwa wyliczenia i każdy nieokreślony moduł wyliczający jest zadeklarowany w zakresie, który natychmiast zawiera specyfikator wyliczenia. Każdy moduł wyliczający o zasięgu jest zadeklarowany w zakresie wyliczenia. Nazwy te są zgodne z regułami zakresu zdefiniowanymi dla wszystkich nazw w (3.3) i (3.4).
Lars Viklund,

1
w C ++ 11 mamy typ std ::aring_type, który zapewnia podstawowy typ wyliczenia. Mamy więc „szablon <typename IntegralType> struct Integral {typedef typename std :: bazowy_typ <IntegralType> :: type Type; }; `W C ++ 14 jest to jeszcze bardziej uproszczone, aby nie używać szablonu <typename IntegralType> struct Integral {typedef std :: bazowy_typ_t <IntegralType> Typ; };
emsr

4

Możesz zdefiniować bezpieczne enum flagi w C ++ 11 za pomocą std::enable_if. Jest to podstawowa implementacja, w której może brakować niektórych rzeczy:

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

Zauważ, że number_of_bitskompilator nie może go wypełnić, ponieważ C ++ nie ma możliwości introspekcji możliwych wartości wyliczenia.

Edycja: Właściwie to jestem poprawiony, można uzyskać number_of_bitsdla ciebie kompilator .

Zauważ, że to może obsłużyć (bardzo nieefektywnie) nieciągły zakres wartości wyliczenia. Powiedzmy, że nie jest dobrym pomysłem stosowanie powyższego z wyliczeniem takim jak ten, w przeciwnym razie nastąpi szaleństwo:

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

Ale wszystko, co uważa się za rozwiązanie, jest w końcu całkiem użytecznym rozwiązaniem. Nie wymaga żadnego kręcenia bitów po stronie użytkownika, jest bezpieczny dla typu i w swoich granicach, tak wydajny, jak to tylko możliwe (mocno opieram się na std::bitsetjakości implementacji ;)).


Jestem pewien, że przeoczyłem niektóre przeciążenia operatorów.
rubenvb,

2

ja nienawidzić nie cierpię makr w moim C ++ 14 tak bardzo, jak następnego faceta, ale zabrałem się do tego wszędzie i całkiem swobodnie:

#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

Użycie tak proste jak

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

I, jak mówią, dowodem jest 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;
}

Zapraszam do niezdefiniowania poszczególnych operatorów według własnego uznania, ale moim bardzo stronniczym zdaniem C / C ++ służy do łączenia się z niskopoziomowymi koncepcjami i strumieniami, i możesz wyciągać te bitowe operatory z moich zimnych, martwych rąk i będę walczył z wszystkimi bezbożnymi makrami i zaklęciami, które potrafię wyczarować, aby je zatrzymać.


2
Jeśli tak bardzo nie cierpisz makr, dlaczego nie użyć właściwej konstrukcji C ++ i napisać niektóre operatory szablonów zamiast makr? Prawdopodobnie podejście oparte na szablonie jest lepsze, ponieważ można użyć opcji std::enable_ifz, std::is_enumaby ograniczyć przeciążenia operatora do pracy tylko z wyliczonymi typami. Dodałem także operatory porównania (za pomocą std::underlying_type) i operator logiczny not, aby dodatkowo wypełnić lukę bez utraty mocnego pisania. Jedyne co mi się nie może dopasować jest niejawna konwersja do bool, ale flags != 0i !flagssą wystarczające dla mnie.
monkey0506,

1

Zwykle należy zdefiniować zestaw wartości całkowitych odpowiadających liczbom binarnym ustawionym na jeden bit, a następnie dodać je razem. Tak zwykle robią to programiści C.

Więc miałbyś (używając operatora bitshift do ustawiania wartości, np. 1 << 2 jest taki sam jak binarny 100)

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

itp

W C ++ masz więcej opcji, zdefiniuj nowy typ raczej int (użyj typedef ) i podobnie ustaw wartości jak wyżej; lub zdefiniuj pole bitowe lub wektor booli . Ostatnie 2 są bardzo wydajne przestrzennie i mają znacznie większy sens w radzeniu sobie z flagami. Pole bitowe ma tę zaletę, że daje ci sprawdzanie typu (a zatem inteligencję).

Powiedziałbym (oczywiście subiektywnie), że programista C ++ powinien użyć pola bitowego dla twojego problemu, ale zwykle widzę podejście #define używane przez programy C często w programach C ++.

Przypuszczam, że pole bitowe jest najbliższe wyliczeniu C #, dlaczego C # próbował przeciążać wyliczenie, aby był typem pola bitowego, jest dziwne - wyliczenie powinno naprawdę być typem pojedynczego wyboru.


11
używanie makr w c ++ w taki sposób jest złe
Bћовић

3
C ++ 14 umożliwia definiowanie literałów binarnych (np. 0b0100), Więc 1 << nformat jest w pewnym sensie przestarzały.
Rob K

Może miałeś na myśli bitset zamiast bitfielda.
Jorge Bellon

1

Krótki przykład flag enum poniżej wygląda prawie jak C #.

O podejściu, moim zdaniem: mniej kodu, mniej błędów, lepszy kod.

#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) jest makrem zdefiniowanym w enum_flags.h (mniej niż 100 linii, można go używać bez ograniczeń).


1
czy plik enum_flags.h jest taki sam jak w 1. wersji twojego pytania? jeśli tak, możesz użyć adresu URL wersji, aby się do niego odnieść: http://programmers.stackexchange.com/revisions/205567/1
wtorek

+1 wygląda dobrze, czysto. Wypróbuję to w naszym projekcie SDK.
Garet Claborn,

1
@GaretClaborn To co Nazwałbym czyste: paste.ubuntu.com/23883996
sehe

1
Oczywiście przegapiłem ::typetam. Poprawiono: paste.ubuntu.com/23884820
sehe

@ hej, kod szablonu nie powinien być czytelny i sensowny. co to za czary? fajnie .... czy ten fragment jest dostępny do użycia lol
Garet Claborn

0

Istnieje jeszcze inny sposób na skórowanie kota:

Zamiast przeciążać operatory bitowe, przynajmniej niektórzy wolą po prostu dodać 4 linijkę, aby pomóc ci obejść to paskudne ograniczenie wyliczeń zakresowych:

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

To prawda, że ​​musisz za ut_cast()każdym razem wpisywać tę rzecz, ale z drugiej strony daje to bardziej czytelny kod, w tym samym sensie, co użycie static_cast<>(), w porównaniu z niejawną konwersją typu lub czymś innym operator uint16_t().

I bądźmy szczerzy tutaj, użycie typu Foojak w powyższym kodzie ma swoje niebezpieczeństwa:

Gdzieś indziej ktoś mógłby zmienić przypadek na zmienną fooi nie oczekiwać, że ma więcej niż jedną wartość ...

Zaśmiecanie kodu ut_cast()pomaga ostrzegać czytelników, że dzieje się coś podejrzanego.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.