Uso delle enfatiche con scope per i bit di bit in C ++

53

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

Le enumerazioni in C # non solo vengono utilizzate per contenere singoli valori, ma anche per contenere combinazioni bitwise di flag, come per Raccomandazione di Microsoft . Tali enumerazioni sono (di solito, ma non necessariamente) decorate con l'attributo [Flags] . Per semplificare la vita degli sviluppatori, gli operatori bit a bit (OR, AND, ecc.) Sono sovraccaricati in modo che tu possa facilmente fare qualcosa di simile a questo (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Sono uno sviluppatore C # esperto, ma ho programmato C ++ solo per un paio di giorni e non sono noto con le convenzioni C ++. Intendo utilizzare un enum di C ++ 11 esattamente nello stesso modo in cui ero abituato a fare in C #. In C ++ 11 gli operatori bit a bit su enumerati non sono sovraccaricati, quindi volevo sovraccaricarli .

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

  1. Una variabile del tipo enum viene utilizzata per contenere il campo di 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 ciò contrasterebbe la filosofia enum strongmente tipizzata delle enunciati scope di C ++ 11.

  2. Utilizza un numero intero semplice se desideri memorizzare una combinazione di enumerazioni bit a bit:

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

    Ma questo ridurrebbe tutto ad un int , lasciandoti senza indizio su 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 di user315052 )

    Ma in questo caso non hai IntelliSense o qualsiasi altro supporto che ti indichi i possibili valori.

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

Naturalmente dal momento che tutti e tre gli approcci funzionano, sono alla ricerca di ragioni tecniche e di fatto, convenzioni generalmente accettate e non semplicemente preferenze personali.

Ad esempio, a causa del mio background C # tendo ad andare con l'approccio 1 in C ++. Questo ha l'ulteriore vantaggio che il mio ambiente di sviluppo può suggerire i possibili valori, e con gli 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 sono d'accordo con me, probabilmente per una buona ragione.

    
posta Daniel Pelsmaeker 09.04.2013 - 13:11
fonte

6 risposte

25

Il modo più semplice è fornire da soli l'overload dell'operatore. Sto pensando di creare una macro per espandere gli overload 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;
}

(nota che type_traits è un'intestazione C ++ 11 e std::underlying_type_t è una funzionalità di C ++ 14.)

    
risposta data 12.07.2013 - 01:29
fonte
6

Storicamente, avrei sempre usato l'enumerazione vecchia (debolmente tipizzata) per nominare le costanti di bit, e ho usato esplicitamente la classe di archiviazione per memorizzare il flag risultante. In questo caso, dovrei accertarmi che le mie numerazioni rientrino nel tipo di archiviazione e tenere traccia dell'associazione tra il campo e le relative costanti.

Mi piace l'idea di enumerazioni strongmente tipizzate, ma non mi piace l'idea che le variabili di tipo enumerato possano contenere valori che non sono tra le costanti di questa enumerazione.

Ad esempio, supponendo che il bit per bit sia stato sovraccaricato:

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 tua terza opzione, hai bisogno di un codice per estrarre il tipo di archiviazione dell'enumerazione. Supponendo di voler forzare un tipo sottostante non firmato (possiamo gestire anche firmato, 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 ancora non ti dà IntelliSense o completamento automatico, ma il rilevamento del tipo di memoria è meno brutto di quanto mi aspettassi inizialmente.

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

enum E4 : int { ... };

Poiché è debolmente digitato e converte implicitamente in / da int (o da qualsiasi tipo di archiviazione scelto), sembra meno strano avere valori che non corrispondono alle costanti enumerate.

Lo svantaggio è che questo è descritto come "di transizione" ...

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

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
    
risposta data 09.04.2013 - 15:35
fonte
3

È possibile definire i flag enum sicuri dal tipo in C ++ 11 utilizzando std::enable_if . Questa è una implementazione rudimentale a cui potrebbero 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 il number_of_bits non può purtroppo essere compilato dal compilatore, in quanto C ++ non ha alcun modo per fare un'introspezione ai possibili valori di un'enumerazione.

Modifica: In realtà sono corretto, è possibile ottenere il compilatore di compilare number_of_bits per te.

Si noti che questo può gestire (in modo inefficace in modo inefficiente) un intervallo di valori di enum non continuo. Diciamo solo che non è una buona idea usare il precedente con un enume come questo o ne deriverà follia:

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

Ma tutto considerato questa è una soluzione abbastanza utilizzabile alla fine. Non ha bisogno di alcun bitfiddling sul lato utente, è sicuro per il tipo e all'interno dei suoi limiti, tanto efficiente quanto diventa (mi sto appoggiando strongmente su std::bitset qualità di implementazione qui ;) ).

    
risposta data 20.12.2016 - 00:31
fonte
2

I odio detesto i macro nel mio C ++ 14 tanto quanto il prossimo, ma ho deciso di usarlo ovunque, e anche molto 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

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 non definire alcuno dei singoli operatori come ritieni opportuno, ma secondo il mio parere strongmente distorto, C / C ++ è per interfacciarsi con concetti e flussi di basso livello, e puoi fare leva su questi operatori bit aberati dal mio freddo , mani morte e ti combatterò con tutte le empie macros e gli incantesimi che lanciano bit che posso evocare per tenerli.

    
risposta data 26.08.2016 - 20:38
fonte
1

Un breve esempio di enum-flag di seguito, assomiglia molto al C #.

Informazioni sull'approccio, a mio parere: 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 linee, liberi di usare senza restrizioni).

    
risposta data 21.07.2013 - 12:42
fonte
0

In genere si definisce un insieme di valori interi che corrispondono a numeri binari a bit singolo, 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 esempio 1 < < 2 è uguale a binary 100)

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

etc

In C ++ hai più opzioni, definisci un nuovo tipo piuttosto che un int (usa typedef ) e impostare in modo simile 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 per gestire le bandiere. Un bitfield ha il vantaggio di darti controllo del tipo (e quindi intellisense).

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

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

    
risposta data 09.04.2013 - 13:54
fonte

Leggi altre domande sui tag