Utilizzo di enumerazioni scoped per bit flag in C ++


60

Un enum X : int(C #) o enum class X : int(C ++ 11) è un tipo che ha un campo interno nascosto intche può contenere qualsiasi valore. Inoltre, un numero di costanti predefinite di Xsono definite sull'enum. È possibile eseguire il cast dell'enum al suo valore intero e viceversa. Questo è vero sia in C # che in C ++ 11.

In C # gli enumeni non sono usati solo per contenere singoli valori, ma anche per contenere combinazioni bit per bit di flag, come da raccomandazione di Microsoft . Tali enum sono (di solito, ma non necessariamente) decorati con l' [Flags]attributo. Per semplificare la vita degli sviluppatori, gli operatori bit a bit (OR, AND, ecc ...) sono sovraccarichi in modo da poter facilmente fare qualcosa del genere (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Sono uno sviluppatore esperto di C #, ma sto programmando C ++ solo da un paio di giorni e non sono noto con le convenzioni C ++. Ho intenzione di usare un enum C ++ 11 nello stesso modo in cui ero abituato a fare in C #. In C ++ 11 gli operatori bit a bit sugli enum con ambito non sono sovraccarichi, quindi volevo sovraccaricarli .

Ciò ha sollecitato un dibattito e le opinioni sembrano variare tra tre opzioni:

  1. Una variabile di tipo enum viene utilizzata per contenere il campo bit, simile a 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)));

    Ma questo contrasterebbe la filosofia enum fortemente tipizzata degli enumerati con scope C ++ 11.

  2. Utilizzare un numero intero semplice se si desidera memorizzare una combinazione bit a bit di enum:

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

    Ma questo ridurrebbe tutto a un int, lasciandoti senza la minima idea di quale tipo dovresti inserire nel metodo.

  3. Scrivi una classe separata che sovraccaricherà gli operatori e manterrà i flag bit a bit in un campo intero nascosto:

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

    ( Codice completo da user315052 )

    Ma allora non hai IntelliSense o qualunque supporto per suggerirti i possibili valori.

So che questa è una domanda soggettiva , ma: quale approccio dovrei usare? Quale approccio, se esiste, è il più ampiamente riconosciuto in C ++? Quale approccio usi quando gestisci i campi di bit e perché ?

Ovviamente, poiché tutti e tre gli approcci funzionano, sto cercando ragioni concrete e tecniche, convenzioni generalmente accettate e non semplicemente preferenze personali.

Ad esempio, a causa del mio background in C #, tendo ad andare con l'approccio 1 in C ++. Questo ha l'ulteriore vantaggio che il mio ambiente di sviluppo può suggerirmi i possibili valori, e con operatori enum sovraccarichi questo è facile da scrivere e capire e abbastanza pulito. E la firma del metodo mostra chiaramente quale tipo di valore si aspetta. Ma la maggior parte delle persone qui non è d'accordo con me, probabilmente per una buona ragione.


2
Il comitato ISO C ++ ha ritenuto l'opzione 1 abbastanza importante da dichiarare esplicitamente che l'intervallo di valori degli enumerati include tutte le combinazioni binarie di flag. (Questo precede C ++ 03) Quindi c'è un'approvazione obiettiva di questa domanda in qualche modo soggettiva.
Salterio

1
(Per chiarire il commento di @MSalters, l'intervallo di un enum C ++ si basa sul suo tipo sottostante (se un tipo fisso), o altrimenti sui suoi enumeratori. In quest'ultimo caso, l'intervallo si basa sul bitfield più piccolo che può contenere tutti gli enumeratori definiti ; ad esempio, per enum E { A = 1, B = 2, C = 4, };, l'intervallo è 0..7(3 bit). Pertanto, lo standard C ++ garantisce esplicitamente che il numero 1 sarà sempre un'opzione praticabile. [In particolare, il enum classvalore predefinito è enum class : intse non diversamente specificato, e quindi ha sempre un tipo sottostante fisso.])
Justin Time 2 Ripristina Monica il

Risposte:


31

Il modo più semplice è quello di fornire all'utente un sovraccarico. Sto pensando di creare una macro per espandere i sovraccarichi di base per tipo.

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

(Si noti che type_traitsè un'intestazione C ++ 11 ed std::underlying_type_tè una funzionalità C ++ 14).


6
std :: sottostanti_tipo_t è C ++ 14. Può usare std :: sottostanti_tipo <T> :: digitare in C ++ 11.
ddevienne,

14
Perché stai usando static_cast<T>per l'input, ma il cast in stile C per il risultato qui?
Ruslan,

2
@Ruslan Ho risposto a questa domanda
audiFanatic l'

Perché ti preoccupi anche di std :: sottostanti_tipo_t quando sai già che è int?
poizan42,

1
Se SBJFrameDragviene definito in una classe e l' |operatore viene successivamente utilizzato nelle definizioni della stessa classe, come definiresti l'operatore in modo tale che possa essere utilizzato all'interno della classe?
Ciao

6

Storicamente, avrei sempre usato la vecchia enumerazione (tipizzata debolmente) per nominare le costanti di bit, e avrei semplicemente usato esplicitamente la classe di archiviazione per memorizzare il flag risultante. Qui spetterebbe a me assicurarmi che le mie enumerazioni si adattino al tipo di archiviazione e di tenere traccia dell'associazione tra il campo e le sue costanti correlate.

Mi piace l'idea di enumerazioni fortemente tipizzate, ma non mi sento a mio agio con l'idea che le variabili di tipo enumerato possano contenere valori che non sono tra le costanti di quell'enumerazione.

Ad esempio, assumendo il bit per bit o è stato sovraccarico:

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

Per la terza opzione, è necessario un po 'di boilerplate per estrarre il tipo di archiviazione dell'enumerazione. Supponendo di voler forzare un tipo sottostante senza segno (possiamo gestire anche il segno, con un po 'più di codice):

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;

Questo non ti dà ancora IntelliSense o il completamento automatico, ma il rilevamento del tipo di archiviazione è meno brutto di quanto mi aspettassi inizialmente.


Ora ho trovato un'alternativa: è possibile specificare il tipo di archiviazione per un'enumerazione debolmente tipizzata. Ha anche la stessa sintassi di in C #

enum E4 : int { ... };

Poiché è tipicamente debolmente e si converte implicitamente in / da int (o qualunque tipo di archiviazione si scelga), è meno strano avere valori che non corrispondono alle costanti enumerate.

Il rovescio della medaglia è che questo è descritto come "di transizione" ...

NB. questa variante aggiunge le sue costanti enumerate sia all'ambito nidificato sia a quello racchiuso, ma puoi aggirare questo con uno spazio dei nomi:

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

1
Un altro aspetto negativo di enumerazioni debolmente tipizzate è che le loro costanti inquinano il mio spazio dei nomi, poiché non hanno bisogno di essere precedute dal nome enum. E ciò può anche causare tutti i tipi di comportamenti strani se si hanno due enumerazioni diverse entrambe con un membro con lo stesso nome.
Daniel AA Pelsmaeker,

È vero. La variante di tipo debole con il tipo di archiviazione specificato aggiunge le sue costanti sia all'ambito che al suo ambito, iiuc.
Inutile

L'enumeratore senza ambito viene dichiarato solo nell'ambito circostante. Essere in grado di qualificarlo con il nome enum fa parte delle regole di ricerca, non della dichiarazione. C ++ 11 7.2 / 10: ogni enum-name e ogni enumeratore senza ambito è dichiarato nell'ambito che contiene immediatamente l'enum-specificator. Ogni enumeratore con ambito è dichiarato nell'ambito dell'enumerazione. Questi nomi obbediscono alle regole di ambito definite per tutti i nomi in (3.3) e (3.4).
Lars Viklund,

1
con C ++ 11 abbiamo std :: sottostanti_tipo che fornisce il tipo sottostante di un enum. Quindi abbiamo 'template <typename IntegralType> struct Integral {typedef typename std :: sottost_tipo <IntegralType> :: type Type; }; `In C ++ 14 questi sono ancora più semplificati nel modello <typename IntegralType> struct Integral {typedef std :: Sottostanti_tipo_t <TipoIntegrale> Tipo; };
emsr

4

È possibile definire flag enum di tipo sicuro in C ++ 11 utilizzando std::enable_if. Questa è un'implementazione rudimentale che potrebbe mancare alcune cose:

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

Nota che number_of_bitspurtroppo non può essere compilato dal compilatore, poiché C ++ non ha alcun modo di fare introspezione sui possibili valori di un elenco.

Modifica: attualmente sono corretto, è possibile ottenere il compilatore number_of_bitsper te.

Nota che questo può gestire (selvaggiamente inefficientemente) un intervallo di valori enum non continui. Diciamo solo che non è una buona idea usare quanto sopra con un enum come questo o ne deriverà la follia:

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

Ma tutto sommato questa è una soluzione abbastanza utilizzabile alla fine. Non ha bisogno di alcun bitfiddling lato utente, è sicuro per i tipi e nei suoi limiti, efficiente quanto riesce (mi sto fortemente appoggiando sulla std::bitsetqualità dell'implementazione qui ;)).


Sono sicuro di aver perso alcuni sovraccarichi degli operatori.
rubenvb,

2

io odiare detesto le macro nel mio C ++ 14 tanto quanto il prossimo, ma ho iniziato a usarlo ovunque, e anche abbastanza liberamente:

#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

Facendo uso semplice come

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

E, come si suol dire, la prova è nel budino:

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

Sentiti libero di definire qualsiasi singolo operatore come ritieni opportuno, ma secondo la mia opinione fortemente distorta, C / C ++ è per interfacciarsi con concetti e flussi di basso livello e puoi estrarre questi operatori bit a bit dalle mie mani fredde e morte e ti combatterò con tutte le macabre macro e gli incantesimi lancianti che posso evocare per tenerli.


2
Se detesti tanto le macro, perché non usare un costrutto C ++ appropriato e scrivere alcuni operatori di template invece delle macro? Probabilmente, l'approccio del modello è migliore perché è possibile utilizzare std::enable_ifcon std::is_enumper limitare i sovraccarichi dell'operatore libero a lavorare solo con i tipi elencati. Ho anche aggiunto operatori di confronto (usando std::underlying_type) e l'operatore non logico per colmare ulteriormente il divario senza perdere la digitazione forte. L'unica cosa che non riesco a corrispondere è implicita conversione a bool, ma flags != 0e !flagssono sufficienti per me.
monkey0506,

1

In genere si definisce un set di valori interi che corrispondono a numeri binari con set di bit singoli, quindi li si sommano. Questo è il modo in cui i programmatori C di solito lo fanno.

Quindi avresti (usando l'operatore bitshift per impostare i valori, ad es. 1 << 2 è uguale al binario 100)

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

eccetera

In C ++ hai più opzioni, definisci un nuovo tipo piuttosto che è un int (usa typedef ) e allo stesso modo imposta i valori come sopra; o definire un bitfield o un vettore di bool . Gli ultimi 2 sono molto efficienti in termini di spazio e hanno molto più senso nel gestire le bandiere. Un bitfield ha il vantaggio di darti il ​​controllo del tipo (e quindi dell'intellisense).

Direi (ovviamente soggettivo) che un programmatore C ++ dovrebbe usare un bitfield per il tuo problema, ma tendo a vedere molto l'approccio #define usato dai programmi C nei programmi C ++.

Suppongo che il bitfield sia il più vicino all'enum di C #, perché C # abbia provato a sovraccaricare un enum per essere un tipo di bitfield è strano - un enum dovrebbe davvero essere un tipo "single-select".


11
usare le macro in c ++ in questo modo è male
BЈовић

3
C ++ 14 consente di definire valori letterali binari (ad esempio 0b0100) in modo che il 1 << nformato sia obsoleto.
Rob K

Forse volevi dire bitset invece di bitfield.
Jorge Bellon,

1

Un breve esempio di enum-flags in basso, assomiglia più o meno a C #.

A proposito dell'approccio, secondo me: meno codice, meno bug, codice migliore.

#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) è una macro, definita in enum_flags.h (meno di 100 righe, libera da usare senza restrizioni).


1
il file enum_flags.h è lo stesso della prima revisione della tua domanda? in caso affermativo, è possibile utilizzare l'URL di revisione per fare riferimento ad esso: http://programmers.stackexchange.com/revisions/205567/1
gnat

+1 sembra buono, pulito. Lo proverò nel nostro progetto SDK.
Garet Claborn,

1
@GaretClaborn Questo è ciò che definirei pulito: paste.ubuntu.com/23883996
visto il

1
Certo, mi mancava il ::typelì. Risolto: paste.ubuntu.com/23884820
visto il

@sehe hey, il codice modello non dovrebbe essere leggibile e sensato. che cos'è questa stregoneria? bello .... è questo frammento aperto per usare lol
Garet Claborn il

0

C'è ancora un altro modo per scuoiare il gatto:

Invece di sovraccaricare gli operatori di bit, almeno alcuni potrebbero preferire semplicemente aggiungere un liner 4 per aiutarti a aggirare quella brutta restrizione degli enumeri scoped:

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

Certo, devi digitare la ut_cast()cosa ogni volta, ma sul lato positivo, questo produce un codice più leggibile, nello stesso senso dell'uso static_cast<>(), rispetto alla conversione implicita del tipo o al operator uint16_t()tipo di cose.

E siamo onesti qui, usare type Foocome nel codice sopra ha i suoi pericoli:

Da qualche altra parte qualcuno potrebbe fare un caso di commutazione su variabile fooe non aspettarsi che contenga più di un valore ...

Così sporcare il codice con ut_cast()aiuta a avvisare i lettori che sta succedendo qualcosa di sospetto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.