web-dev-qa-db-fra.com

À la poursuite d'un meilleur enum bitflag

D'accord, nous en sommes à C++ 17 et il n'y a toujours pas de réponse satisfaisante à une très bonne interface bitflags en C++. 

Nous avons enum qui saignent leurs valeurs de membre dans la portée englobante, mais convertissent implicitement en leur type sous-jacent, ils peuvent donc être utilisés tels quels, mais refusent de se réaffecter à l'énum sans être exprimés.

Nous avons enum class qui résout le problème de la portée du nom, de sorte que leurs valeurs doivent être explicitement nommées MyEnum::MyFlag ou même MyClass::MyEnum::MyFlag, mais elles ne sont pas converties implicitement en leur type sous-jacent, elles ne peuvent donc pas être utilisées comme indicateurs de bit sans transtensions sans fin.

Et enfin, nous avons les anciens champs de bits de C tels que:

struct FileFlags {
   unsigned ReadOnly : 1;
   unsigned Hidden : 1;
   ...
};

Ce qui présente l’inconvénient de ne pouvoir s’initialiser dans son ensemble - il faut recourir à memset ou transtyper l’adresse ou similaire pour écraser la valeur entière ou l’initialiser en une fois ou manipuler plusieurs bits à la fois. Il souffre également de ne pas pouvoir nommer la valeur d'un indicateur donné, par opposition à son adresse - il n'y a donc aucun nom représentant 0x02, alors qu'il existe un tel nom lors de l'utilisation d'énums, il est donc facile de nommer une combinaison de drapeaux, tels que FileFlags::ReadOnly | FileFlags::Hidden- il n’est tout simplement pas un bon moyen d’en dire autant pour les champs de bits.

De plus, nous avons toujours une simple variable constexpr ou #define pour nommer les valeurs de bits, puis nous n’utilisons tout simplement pas d’énumérations. Cela fonctionne, mais dissocie complètement les valeurs de bit du type bitflag sous-jacent. Peut-être que cette approche n’est finalement pas la pire, en particulier si les valeurs des indicateurs de bits sont constexpr au sein d’une structure pour leur donner leur propre nom-portée?

struct FileFlags {
    constexpr static uint16_t ReadOnly = 0x01u;
    constexpr static uint16_t Hidden = 0x02u;
    ...
}

Donc, dans l’état actuel des choses, nous avons beaucoup de techniques, dont aucune n’est un moyen vraiment solide de dire: 

Voici un type qui contient les drapeaux de bits valides suivants, son propre nom de domaine, et ces bits et ce type doivent pouvoir être utilisés librement avec des opérateurs de bits standards, tels que | & ^ ~, et elles doivent être comparables aux valeurs intégrales telles que 0, et le résultat de tout opérateur au niveau des bits doit rester le type nommé et ne pas évoluer en une intégrale.

Cela dit, un certain nombre de tentatives ont été tentées pour essayer de produire l'entité ci-dessus en C++ - 

  1. L’équipe Windows OS a développé une macro simple qui génère du code C++ pour définir les opérateurs manquants nécessaires sur un type d’énumération donné DEFINE_ENUM_FLAG_OPERATORS(EnumType), qui définit ensuite opérateur | & ^ ~ et les opérations associées telles que | = et etc.
  2. 'grisumbras' a un projet GIT public permettant d'activer la sémantique de bitflag avec des énumérations étendues ici , qui utilise la méta-programmation enable_if pour permettre à une énumération donnée de se convertir en un type indicateur-bit prenant en charge les opérateurs manquants et inversement.
  3. Sans connaître ce qui précède, j’ai écrit un wrapper bit_flags relativement simple qui définit tous les opérateurs au niveau du bit sur lui-même, de sorte que l’on puisse utiliser un bit_flags<EnumType> flags et ensuite une variable sémantique flags. Cela ne permet pas à la base énumérée de gérer correctement les opérateurs au niveau des bits directement. Vous ne pouvez donc pas dire EnumType::ReadOnly | EnumType::Hidden, même si vous utilisez un bit_flags<EnumType>, car l’énumération sous-jacente elle-même ne prend toujours pas en charge les opérateurs nécessaires. Je devais finir par faire la même chose, essentiellement, en tant que n ° 1 et n ° 2 ci-dessus, et activer operator | (EnumType, EnumType) pour les différents opérateurs au niveau du bit en demandant aux utilisateurs de déclarer une spécialisation pour un méta-type pour leur énumération, tel que template <> struct is_bitflag_enum<EnumType> : std::true_type {};.

En fin de compte, le problème avec les n ° 1, n ° 2 et n ° 3 est qu’il n’est pas possible (pour autant que je sache) de définir les opérateurs manquants sur l’énumération elle-même (comme dans N ° 1) ou de définir le type de facilitateur nécessaire ( Par exemple, template <> struct is_bitflag_enum<EnumType> : std::true_type {}; en n ° 2 et partiellement en n ° 3) au sein de la classe. Celles-ci doivent se produire en dehors d'une classe ou d'une structure, car C++ n'a tout simplement pas de mécanisme à ma connaissance qui me permettrait de faire de telles déclarations au sein d'une classe.

Alors maintenant, je souhaite avoir un ensemble d'indicateurs qui devrait être limité à une classe donnée, mais je ne peux pas utiliser ces indicateurs dans l'en-tête de la classe (par exemple, initialisation par défaut, fonctions inline, etc.) car je ne peux activer aucun des paramètres. des machines qui permettent de traiter l’énum en tant que bitflags jusqu’à la fin de l’attelle de fermeture pour la définition de classe Ou bien, je peux définir toutes ces énumérations-drapeaux en dehors de la classe à laquelle elles appartiennent, de sorte que je puisse ensuite invoquer le "transformer cette énumération en un type au niveau du bit" avant la définition de la classe utilisateur, afin d'utiliser pleinement cette fonctionnalité dans la classe client - mais à présent, les indicateurs de bits sont dans la portée externe au lieu d'être associés à la classe elle-même.

Ce n'est pas la fin du monde - rien de ce qui précède ne l'est. Mais tout cela provoque des maux de tête sans fin lors de l'écriture de mon code - et m'empêche de l'écrire de la manière la plus naturelle - c'est-à-dire avec un flag-enum donné qui appartient à une classe spécifique au sein de cette classe de client, mais avec un indicateur bit à bit -semantics (mon approche n ° 3 permet presque ceci - tant que tout est entouré d'un bit_flags - permet explicitement d'activer la compatibilité bit à bit nécessaire).

Tout cela me laisse encore le sentiment agaçant que cela pourrait être bien meilleur qu’il ne l’est! 

Il devrait sûrement y avoir - et c'est peut-être mais je ne l'ai pas encore compris - une approche enum permettant d'activer des opérateurs au niveau du bit tout en leur permettant d'être déclarés et utilisés dans un champ de classe englobant ...

Quelqu'un a-t-il une approche ou une approche que je n'ai pas envisagée ci-dessus, qui me permettrait "le meilleur des mondes possibles" sur ce sujet?

17
Mordachai

Par exemple

// union only for convenient bit access. 
typedef union a
{ // it has its own name-scope
    struct b
     {
         unsigned b0 : 1;
         unsigned b2 : 1;
         unsigned b3 : 1;
         unsigned b4 : 1;
         unsigned b5 : 1;
         unsigned b6 : 1;
         unsigned b7 : 1;
         unsigned b8 : 1;
         //...
     } bits;
    unsigned u_bits;
    // has the following valid bit-flags in it
    typedef enum {
        Empty = 0u,
        ReadOnly = 0x01u,
        Hidden  = 0x02u
    } Values;
    Values operator =(Values _v) { u_bits = _v; return _v; }
     // should be freely usable with standard bitwise operators such as | & ^ ~   
    union a& operator |( Values _v) { u_bits |= _v; return *this; }
    union a& operator &( Values _v) { u_bits &= _v; return *this; }
    union a& operator |=( Values _v) { u_bits |= _v; return *this; }
    union a& operator &=( Values _v) { u_bits &= _v; return *this; }
     // ....
    // they should be comparable to integral values such as 0
    bool operator <( unsigned _v) { return u_bits < _v; }
    bool operator >( unsigned _v) { return u_bits > _v; }
    bool operator ==( unsigned _v) { return u_bits == _v; }
    bool operator !=( unsigned _v) { return u_bits != _v; }
} BITS;


int main()
 {
     BITS bits;
     int integral = 0;

     bits = bits.Empty;

     // they should be comparable to integral values such as 0
     if ( bits == 0)
     {
         bits = bits.Hidden;
         // should be freely usable with standard bitwise operators such as | & ^ ~
         bits = bits | bits.ReadOnly;
         bits |= bits.Hidden;
         // the result of any bitwise operators should remain the named type, and not devolve into an integral
         //bits = integral & bits; // error
         //bits |= integral; // error
     }
 }
1
Andrey Sv

J'adopte l'approche de Xaqq's FlagSet sur Code Review SE .

La clé est d'introduire un nouveau type qui servira de "conteneur" pour une ou plusieurs valeurs activées à partir d'une liste d'options fixe. Ledit conteneur est un wrapper autour de bitset qui prend, en entrée, les instances d’une énumération scoped.

Grâce à l'énumération Scoped, il est conforme aux types et peut effectuer des opérations de type binaire via une délégation de surcharge d'opérateur aux opérations de jeu de bits. Et vous pouvez toujours utiliser l'énumération scoped directement si vous le souhaitez, et si vous n'avez pas besoin des opérations au niveau des bits ou pour stocker plusieurs indicateurs.

Pour la production, j'ai apporté quelques modifications au code lié; quelques-uns d'entre eux sont discutés dans les commentaires sur la page de révision du code.

0

J'utilise enum class avec les opérateurs basés sur des modèles suivants:

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM operator |( ENUM lhs, ENUM rhs )
{
    return static_cast< ENUM >( static_cast< UInt32 >( lhs ) | static_cast< UInt32 >( rhs ));
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator |=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs | rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline UInt32 operator &( ENUM lhs, ENUM rhs )
{
    return static_cast< UInt32 >( lhs ) & static_cast< UInt32 >( rhs );
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, ENUM rhs )
{
    lhs = lhs & rhs;
    return lhs;
}

template< typename ENUM, typename std::enable_if< std::is_enum< ENUM >::value, int >::type* = nullptr >
inline ENUM& operator &=( ENUM& lhs, int rhs )
{
    lhs = static_cast< ENUM >( static_cast< int >( lhs ) & rhs );
    return lhs;
}

Si vous craignez que les opérateurs ci-dessus ne se retrouvent dans d'autres enums, vous pouvez les encapsuler dans le même espace de noms que celui où l'énum est déclaré, ou même simplement les implémenter sur une base énumération par énumération (j'avais l'habitude d'utiliser une macro pour cette). De manière générale cependant, j’ai considéré cet excès et je les ai maintenant déclarés dans mon espace de noms de premier niveau, quel que soit le code à utiliser.

0
James