web-dev-qa-db-fra.com

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

Une enum X : int (C #) ou enum class X : int (C++ 11) est un type qui a un champ interne caché de int qui peut contenir n'importe quelle valeur. De plus, un certain nombre de constantes prédéfinies de X sont définies sur l'énumération. Il est possible de convertir l'énumération à sa valeur entière et vice versa. Tout cela est vrai à la fois en C # et C++ 11.

En C #, les énumérations ne sont pas seulement utilisées pour contenir des valeurs individuelles, mais aussi pour contenir des combinaisons binaires de drapeaux, selon recommandation de Microsoft . Ces énumérations sont (généralement, mais pas nécessairement) décorées avec le [Flags] attribut. Pour faciliter la vie des développeurs, les opérateurs au niveau du bit (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 le C++ que depuis quelques jours maintenant, et je ne connais pas les conventions C++. J'ai l'intention d'utiliser une énumération C++ 11 de la même manière que je l'avais l'habitude de faire en C #. En C++ 11, les opérateurs au niveau du bit sur les énumérations étendues ne sont pas surchargés, donc je voulais les surcharger .

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

  1. Une variable de 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 contredirait 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'énumérations 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 aucun indice sur le type que vous êtes censé mettre dans la méthode.

  3. Écrivez une classe distincte qui surchargera les opérateurs et maintiendra les drapeaux 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 ser315052 )

    Mais alors vous n'avez pas d'IntelliSense ou quelque support que ce soit 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, puisque les trois approches fonctionnent, je recherche des raisons factuelles et techniques, des conventions généralement acceptées et pas simplement des préférences personnelles.

Par exemple, en raison de mes antécédents en C #, j'ai tendance à suivre l'approche 1 en C++. Cela a l'avantage supplémentaire que mon environnement de développement peut m'indiquer les valeurs possibles, et avec des opérateurs enum surchargés, cela est facile à écrire et à comprendre, et assez propre. Et la signature de la méthode montre clairement quel type de valeur elle attend. Mais la plupart des gens ici ne sont pas d'accord avec moi, probablement pour une bonne raison.

63

Le moyen le plus simple est de fournir à l'opérateur vous-même des surcharges. Je pense à créer une macro pour étendre 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 que type_traits est un en-tête C++ 11 et std::underlying_type_t est une fonctionnalité C++ 14.)

32
Dave

Vous pouvez définir des indicateurs d'énumération de type sécurisé dans C++ 11 en utilisant std::enable_if. Il s'agit d'une implémentation rudimentaire qui peut manquer 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() 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;
}

Noter la number_of_bits ne peut malheureusement pas être rempli par le compilateur, car C++ n'a aucun moyen d'introspecter les valeurs possibles d'une énumération.

Edit: En fait, je suis corrigé, il est possible d'obtenir le remplissage du compilateur number_of_bits pour vous.

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

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

Mais tout bien considéré, c'est une solution tout à fait utilisable au final. N'a pas besoin de bitfiddling côté utilisateur, est de type sécurisé et dans ses limites, aussi efficace que possible (je m'appuie fortement sur std::bitset qualité d'implémentation ici ;)).

6
rubenvb

Historiquement, j'aurais toujours utilisé l'ancienne énumération (faiblement typée) pour nommer les constantes de bits, et j'utilisais simplement la classe de stockage explicitement pour stocker l'indicateur résultant. Ici, il m'incomberait de m'assurer que mes énumérations cadrent avec le type de stockage et de garder une trace de l'association entre le champ et ses constantes connexes.

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

Par exemple, en supposant que le bit 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 3e option, vous avez besoin d'un passe-partout pour extraire le type de stockage de l'énumération. En supposant que nous voulons forcer un type sous-jacent non signé (nous pouvons également gérer 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 ou la saisie semi-automatique, mais la détection du type de stockage est moins moche que ce à quoi je m'attendais à 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 { ... };

Parce qu'il est faiblement typé et convertit implicitement en/de int (ou quel que soit le type de stockage que vous choisissez), il semble 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 à la fois à l'étendue imbriquée et à l'étendue englobante, mais vous pouvez contourner cela avec un espace de noms:

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

Je haine déteste les macros dans mon C++ 14 autant que le prochain, mais j'ai pris l'habitude de 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

Rendre l'utilisation 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 à définir l'un des opérateurs individuels comme bon vous semble, mais à mon avis très biaisé, C/C++ est destiné à interfacer avec des concepts et des flux de bas niveau, et vous pouvez extraire ces opérateurs au niveau du bit de mes mains froides et mortes. et je vous combattrai avec toutes les macros impies et les sorts renversants que je peux conjurer pour les garder.

3
Mahmoud Al-Qudsi

En règle générale, vous définissez un ensemble de valeurs entières qui correspondent à des nombres binaires définis sur un seul bit, puis les ajoutez ensemble. C'est la façon dont 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 le même que 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 (utilisez typedef ) et définissez les valeurs de la même manière que ci-dessus; ou définissez un champ de bits ou un vecteur de bools . Les 2 derniers sont très économes en espace et ont beaucoup plus de sens pour traiter les drapeaux. Un champ de bits a l'avantage de vous donner la vérification de type (et donc l'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 essayé de surcharger une énumération pour être un type de champ de bits est étrange - une énumération devrait vraiment être un type "à sélection unique".

1
gbjbaanb

Un bref exemple d'enum-flags ci-dessous, ressemble à peu près à 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 aucune restriction).

1
Yuri Yaryshev

Il existe encore une autre façon de dépouiller le chat:

Au lieu de surcharger les opérateurs de bits, au moins certains pourraient préférer simplement ajouter un liner 4 pour vous aider à contourner cette restriction désagréable des énumérations de portée:

#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 chose ut_cast() à chaque fois, mais à la hausse, cela donne un code plus lisible, dans le même sens que l'utilisation de static_cast<>() le fait, par rapport à la conversion de type implicite ou operator uint16_t() genre de choses.

Et soyons honnêtes ici, utiliser le type Foo comme dans le code ci-dessus a ses dangers:

Quelque part ailleurs, quelqu'un pourrait faire un changement de casse sur la variable foo et ne pas s'attendre à ce qu'elle contienne plus d'une valeur ...

Donc, joncher le code avec ut_cast() permet d'alerter les lecteurs que quelque chose de louche se passe.

0
BitTickler