Utilisation d'énumérations étendues pour les indicateurs de bits en C ++

60

Un enum X : int(C #) ou enum class X : int(C ++ 11) est un type qui a un champ interne caché intpouvant contenir n'importe quelle valeur. De plus, un nombre de constantes prédéfinies de Xsont définies sur l'énum. Il est possible de convertir l'énum en son entier et inversement. Ceci est vrai à la fois en C # et en C ++ 11.

En C #, les énumérations ne sont pas seulement utilisées pour conserver des valeurs individuelles, mais également pour contenir des combinaisons d'indicateurs au niveau du bit, conformément à la recommandation de Microsoft . Ces énumérations sont (généralement, mais pas nécessairement) décorées avec l' [Flags]attribut. Pour faciliter la vie des développeurs, les opérateurs au niveau des bits (OR, AND, etc ...) sont surchargés afin que vous puissiez facilement faire quelque chose comme ça (C #):

void M(NumericType flags);

M(NumericType.Sign | NumericType.ZeroPadding);

Je suis un développeur C # expérimenté, mais je ne programme C ++ que depuis quelques jours maintenant et je ne suis pas connu des conventions C ++. J'ai l'intention d'utiliser un enum C ++ 11 exactement de la même manière que j'avais l'habitude de le faire en C #. En C ++ 11, les opérateurs de bits sur les énumérations étendues ne sont pas surchargés. Je voulais donc les surcharger .

Cela a suscité un débat et les opinions semblent varier entre trois options:

  1. Une variable du type enum est utilisée pour contenir le champ de bits, similaire à 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)));

    Mais cela irait à l’encontre de la philosophie d’énumération fortement typée des énumérations étendues de C ++ 11.

  2. Utilisez un entier simple si vous souhaitez stocker une combinaison d'énums au niveau du bit:

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

    Mais cela réduirait tout à un int, ne vous laissant aucune idée du type que vous êtes censé mettre dans la méthode.

  3. Ecrivez une classe séparée qui surchargera les opérateurs et maintiendra les indicateurs au niveau du bit dans un champ entier caché:

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

    ( code complet par utilisateur315052 )

    Mais alors vous n’avez pas d’IntelliSense ni aucun autre support pour vous indiquer les valeurs possibles.

Je sais que c’est une question subjective , mais: quelle approche dois-je utiliser? Quelle approche, le cas échéant, est la plus largement reconnue en C ++? Quelle approche utilisez-vous pour traiter les champs de bits et pourquoi ?

Bien sûr, étant donné que les trois approches fonctionnent, je cherche des raisons factuelles et techniques, des conventions généralement acceptées, et pas simplement des préférences personnelles.

Par exemple, à cause de mon arrière-plan C #, j'ai tendance à choisir l'approche 1 en C ++. Cela présente l'avantage supplémentaire que mon environnement de développement peut m'indiquer les valeurs possibles. Avec les opérateurs d'enum surchargés, il est facile à écrire et à comprendre, et plutôt propre. Et la signature de la méthode indique clairement le type de valeur qu’elle attend. Mais la plupart des gens ici ne sont pas d'accord avec moi, probablement pour une bonne raison.

Daniel AA Pelsmaeker
la source
2
Le comité ISO C ++ a jugé l'option 1 suffisamment importante pour indiquer explicitement que la plage de valeurs des énumérations comprend toutes les combinaisons binaires d'indicateurs. (Ceci est antérieur à C ++ 03) Il y a donc une approbation objective de cette question quelque peu subjective.
MSalters
1
(Pour clarifier le commentaire de @MSalters, la plage d'une énumération C ++ est basée sur son type sous-jacent (s'il s'agit d'un type fixe) ou autrement sur ses énumérateurs. Dans ce dernier cas, la plage est basée sur le plus petit champ pouvant contenir tous les énumérateurs définis. ; par exemple, pour enum E { A = 1, B = 2, C = 4, };, la plage est 0..7(3 bits). Ainsi, la norme C ++ garantit explicitement que # 1 sera toujours une option viable. [Spécifiquement, sauf si spécifié autrement , par enum classdéfaut enum class : int, et a donc toujours un type sous-jacent fixe.])
Justin Time 2 Réintégrez Monica le

Réponses:

31

Le moyen le plus simple consiste à fournir à l’opérateur les surcharges vous-même. Je songe à créer une macro pour développer les surcharges de base par type.

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

(Notez qu'il type_traitss'agit d'un en-tête C ++ 11 et d' std::underlying_type_tune fonctionnalité C ++ 14.)

Dave
la source
6
std :: hidden_type_t est C ++ 14. Peut utiliser std :: hidden_type <T> :: type en C ++ 11.
jeudi
14
Pourquoi utilisez-vous static_cast<T>pour la saisie, mais la conversion de style C pour le résultat ici?
Ruslan
2
@Ruslan J'appuie cette question
audiFanatic
Pourquoi vous embêtez même avec std :: sous_type_t alors que vous savez déjà que c'est int?
poizan42
1
Si SBJFrameDragest défini dans une classe et que l' |opérateur est utilisé ultérieurement dans les définitions de la même classe, comment définiriez-vous l'opérateur de sorte qu'il puisse être utilisé dans la classe?
HelloGoodbye
6

Historiquement, j'aurais toujours utilisé l'ancienne énumération (faiblement typée) pour nommer les constantes de bits, et simplement utilisé explicitement la classe de stockage pour stocker l'indicateur résultant. Ici, c’est à moi qu’il incombe de s’assurer que mes énumérations correspondent au type de stockage et de garder une trace de l’association entre le champ et ses constantes connexes.

J'aime l'idée des énumérations fortement typées, mais je ne suis pas très à l'aise avec l'idée que les variables de type énuméré puissent contenir des valeurs qui ne figurent pas parmi les constantes de cette énumération.

Par exemple, en supposant que le bitwise ou a été surchargé:

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

Pour votre 3ème option, vous avez besoin d'un passe-partout pour extraire le type de stockage de l'énumération. En supposant que nous voulions forcer un type sous-jacent non signé (nous pouvons aussi manipuler signé, avec un peu plus de code):

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;

Cela ne vous donne toujours pas IntelliSense ni l'auto-complétion, mais la détection du type de stockage est moins laide que prévu à l'origine.


Maintenant, j'ai trouvé une alternative: vous pouvez spécifier le type de stockage pour une énumération faiblement typée. Il a même la même syntaxe qu'en C #

enum E4 : int { ... };

Comme il est faiblement typé et qu'il convertit implicitement vers / depuis int (ou le type de stockage que vous choisissez), il est moins étrange d'avoir des valeurs qui ne correspondent pas aux constantes énumérées.

L'inconvénient est que cela est décrit comme "transitoire" ...

NB cette variante ajoute ses constantes énumérées aux portées imbriquée et englobante, mais vous pouvez contourner ce problème avec un espace de noms:

namespace E5 {
    enum Enum : int { A, B, C };
}
E5::Enum x = E5::A; // or E5::Enum::A
Inutile
la source
1
Un autre inconvénient des énumérations faiblement typées est que leurs constantes polluent mon espace de noms, car elles n'ont pas besoin de préfixer le nom de l'énum. Et cela peut aussi causer toutes sortes de comportements étranges si vous avez deux enums différents avec un membre du même nom.
Daniel AA Pelsmaeker
C'est vrai. La variante faiblement typée avec le type de stockage spécifié ajoute ses constantes à la fois à la portée englobante et à sa propre portée, iiuc.
Inutile
L'énumérateur non couvert est uniquement déclaré dans la portée environnante. Pouvoir le qualifier par le nom-enum fait partie des règles de recherche, pas de la déclaration. C ++ 11 7.2 / 10: Chaque nom-énumération et chaque énumérateur non récursif est déclaré dans la portée qui contient immédiatement le spécificateur enum. Chaque énumérateur à portée est déclaré dans la portée de l'énumération. Ces noms obéissent aux règles de portée définies pour tous les noms dans (3.3) et (3.4).
Lars Viklund
1
Avec C ++ 11, nous avons std :: sous_type qui fournit le type sous-jacent d'un enum. Nous avons donc 'template <type_type_intégrateur> struct Integral {typedef nom_type std :: sous_type <type_intégrateur> :: type Type; }; `En C ++ 14, il est encore plus simple de modéliser <typename IntegralType> struct Integral {typedef std :: sous_type_type_t <IntegralType> Type; };
Emsr
4

Vous pouvez définir des indicateurs d'énumération sécurisés pour le type dans C ++ 11 à l'aide de std::enable_if. Ceci est une implémentation rudimentaire qui manque peut-être à certaines choses:

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

Notez que le number_of_bitscompilateur ne peut malheureusement pas remplir le formulaire, car C ++ ne dispose d'aucun moyen d'introspection quant aux valeurs possibles d'une énumération.

Edit: En fait, je suis corrigé, il est possible de remplir le compilateur number_of_bitspour vous.

Notez que cela peut gérer (de manière inefficace) une plage de valeurs enum non continue. Disons simplement que ce n'est pas une bonne idée d'utiliser ce qui précède avec une telle énumération, sinon la folie s'ensuivra:

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

Mais toutes choses considérées, c’est finalement une solution tout à fait utilisable. N'a pas besoin de bidouillage côté utilisateur, il est sécuritaire en ce qui concerne les types et aussi efficace qu'il soit (j'appuie fortement sur la std::bitsetqualité de la mise en oeuvre ici ;)).

Rubenvb
la source
Je suis sûr d'avoir manqué certaines surcharges des opérateurs.
rubenvb
2

je haine déteste les macros dans mon C ++ 14 autant que le prochain, mais je me suis mis à l’utiliser partout, et assez généreusement aussi:

#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

Faire usage aussi simple que

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

Et, comme on dit, la preuve est dans le 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;
}

N'hésitez pas à choisir l'un des opérateurs individuels comme bon vous semble, mais à mon avis, le C / C ++ sert d'interface avec les concepts et les flux de bas niveau, et vous pouvez les extraire de mes mains mortes et froides. et je vais vous battre avec toutes les macros impies et les sorts peu renversants que je peux conjurer pour les garder.

Mahmoud Al-Qudsi
la source
2
Si vous détestez autant les macros, pourquoi ne pas utiliser une construction C ++ appropriée et écrire des opérateurs de modèles au lieu des macros? On peut soutenir que l'approche par modèle est préférable car vous pouvez utiliser std::enable_ifavec std::is_enumpour limiter les surcharges de votre opérateur libre au travail avec les types énumérés. J'ai également ajouté des opérateurs de comparaison (using std::underlying_type) et l'opérateur non logique pour combler davantage l'écart sans perdre le typage fort. La seule chose que je ne peux pas répondre est la conversion implicite bool, mais flags != 0et !flagssont suffisantes pour moi.
Monkey0506
1

Généralement, vous définissez un ensemble de valeurs entières correspondant à des nombres binaires définis sur un seul bit, puis vous les additionnez. C’est ainsi que les programmeurs C le font habituellement.

Donc vous auriez (en utilisant l'opérateur bitshift pour définir les valeurs, par exemple 1 << 2 est identique à binaire 100)

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

etc

En C ++, vous avez plus d’options, définissez un nouveau type plutôt qu’un int (use typedef ) et définissez les mêmes valeurs que ci-dessus; ou définir un bitfield ou un vecteur de bools . Les deux derniers sont très efficaces en termes d'espace et sont beaucoup plus utiles pour gérer les drapeaux. Un champ de bits a l'avantage de vous donner la vérification de type (et donc, intellisense).

Je dirais (évidemment subjectif) qu'un programmeur C ++ devrait utiliser un champ de bits pour votre problème, mais j'ai tendance à voir l'approche #define utilisée par les programmes C beaucoup dans les programmes C ++.

Je suppose que le champ de bits est le plus proche de l'énumération de C #. Pourquoi C # a-t-il essayé de surcharger un énum pour qu'il soit un type de champ de bits?

gbjbaanb
la source
11
utiliser des macros dans c ++ de cette manière est mauvais
Bовић
3
C ++ 14 vous permet de définir des littéraux binaires (par exemple 0b0100) afin que le 1 << nformat soit en quelque sorte obsolète.
Rob K
Peut-être que vous vouliez dire bitset au lieu de bitfield.
Jorge Bellon
1

Un court exemple d'énum-flags ci-dessous ressemble beaucoup à C #.

À propos de l'approche, à mon avis: moins de code, moins de bugs, un meilleur 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) est une macro, définie dans enum_flags.h (moins de 100 lignes, libre d'utilisation sans restrictions).

Yuri Yaryshev
la source
1
Le fichier enum_flags.h est -il identique à celui de la 1ère révision de votre question? Si oui, vous pouvez utiliser l'URL de révision pour y faire référence: http://programmers.stackexchange.com/revisions/205567/1
gnat le
+1 semble bon, propre. Je vais essayer cela dans notre projet de SDK.
Garet Claborn
1
Ce @GaretClaborn est ce que je qualifierais propre: paste.ubuntu.com/23883996
sehe
1
Bien sûr, raté le ::typelà. Correction: paste.ubuntu.com/23884820
sehe
@sehe hey, le code de gabarit n'est pas censé être lisible et n'a de sens. Quelle est cette sorcellerie? nice .... est ce bout ouvert à utiliser lol
Garet Claborn
0

Il y a encore une autre façon de peler le chat:

Au lieu de surcharger les opérateurs de bits, au moins certains préfèreraient peut-être simplement ajouter un liner 4 pour vous aider à contourner cette restriction désagréable des enums étendus:

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

Certes, vous devez taper la ut_cast()chose à chaque fois, mais du côté positif, cela donne un code plus lisible, au même sens que l’utiliser static_cast<>(), par rapport à la conversion de type implicite ou à une operator uint16_t()sorte de chose.

Et soyons honnêtes ici, utiliser le type Foocomme dans le code ci-dessus présente des dangers:

Quelque part, quelqu'un pourrait faire un changement de casse sur une variable fooet ne pas s'attendre à ce qu'il contienne plus d'une valeur ...

Donc, jeter un peu de code dans le code ut_cast()aide à prévenir les lecteurs que quelque chose de louche se passe.

BitTickler
la source