Comment utiliser les énumérations comme indicateurs en C ++?


187

Traiter les enums comme des indicateurs fonctionne bien en C # via l' [Flags]attribut, mais quelle est la meilleure façon de le faire en C ++?

Par exemple, j'aimerais écrire:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};

seahawk.flags = CanFly | EatsFish | Endangered;

Cependant, j'obtiens des erreurs de compilation concernant int/ enumconversions. Existe-t-il une meilleure façon d'exprimer cela qu'un simple casting brutal? De préférence, je ne veux pas me fier à des constructions de bibliothèques tierces telles que boost ou Qt.

EDIT: Comme indiqué dans les réponses, je peux éviter l'erreur du compilateur en déclarant seahawk.flagscomme int. Cependant, j'aimerais avoir un mécanisme pour appliquer la sécurité de type, afin que quelqu'un ne puisse pas écrire seahawk.flags = HasMaximizeButton.


Autant que je sache dans Visual C ++ 2013, l' [Flags]attribut fonctionne très bien, c'est-à-dire:[Flags] enum class FlagBits{ Ready = 1, ReadMode = 2, WriteMode = 4, EOF = 8, Disabled = 16};
rivanov

@rivanov, Non, cela ne fonctionne pas avec C ++ (2015 aussi). Voulez-vous dire C #?
Ajay

5
@rivanov, L'attribut [Flags] fonctionne uniquement avec le .Net Framework dans C ++ CLI, le C ++ natif ne prend pas en charge ces attributs.
Zoltan Tirinda le

Réponses:


251

La manière "correcte" est de définir des opérateurs de bits pour l'énumération, comme:

enum AnimalFlags
{
    HasClaws   = 1,
    CanFly     = 2,
    EatsFish   = 4,
    Endangered = 8
};

inline AnimalFlags operator|(AnimalFlags a, AnimalFlags b)
{
    return static_cast<AnimalFlags>(static_cast<int>(a) | static_cast<int>(b));
}

Etc. reste des opérateurs de bits. Modifiez si nécessaire si la plage d'énumération dépasse la plage int.


42
^ ceci. La seule question est de savoir comment automatiser / modéliser les définitions d'opérateurs afin que vous n'ayez pas à les définir constamment à chaque fois que vous ajoutez une nouvelle énumération.
eodabash

10
De plus, le transtypage d'un entier arbitraire vers le type enum est-il valide, même si la valeur int ne correspond à aucun des identificateurs de l'énumération?
Ingo Schalk-Schupp

8
C'est un non-sens complet. Quel membre de AnimalFlagsest représenté par l'expression HasClaws | CanFly? Ce n'est pas à cela que enumservent les s. Utilisez des entiers et des constantes.
Courses de légèreté en orbite

26
@LightnessRacesinOrbit: Ce n'est pas correct. Le domaine d'un type enum est le domaine de son type sous-jacent - c'est seulement que certains ont reçu un nom. Et pour répondre à votre question: Le membre " (HasClaws | CanFly)".
Xeo

5
@MarcusJ: restreindre vos valeurs à des puissances de 2 vous permet d'utiliser vos énumérations comme bit-flags. Ainsi, si vous obtenez un 3, vous le savez à la fois HasClaws(= 1) et CanFly(= 2). Si, à la place, vous attribuez simplement les valeurs 1 à 4 et que vous obtenez un 3, il peut s'agir d'un simple EatsFishou encore d'une combinaison de HasClawset CanFly. Si votre énumération ne dénote que des états exclusifs, les valeurs consécutives conviennent, mais une combinaison d'indicateurs nécessite que les valeurs soient exclusives au bit.
Christian Severin

122

Remarque (également un peu hors sujet): Une autre façon de créer des indicateurs uniques peut être effectuée en utilisant un décalage de bits. Moi-même, je trouve cela plus facile à lire.

enum Flags
{
    A = 1 << 0, // binary 0001
    B = 1 << 1, // binary 0010
    C = 1 << 2, // binary 0100
    D = 1 << 3, // binary 1000
};

Il peut contenir des valeurs jusqu'à un int, c'est-à-dire, la plupart du temps, 32 indicateurs, ce qui est clairement reflété dans la quantité de décalage.


2
Pourriez-vous s'il vous plaît supprimer la dernière virgule (3,) et ajouter un deux-points après} pour rendre le code facile à copier et coller? Merci
Katu

4
Aucune mention d'hexidécimal? Blasphème!
Pharap

1
@Jamie, les cardinaux commencent toujours par 1, seuls les ordinaux peuvent commencer par 0 ou 1, selon à qui vous parlez.
Michael

2
@Michael, c'est vrai! Dans une énumération, vous réservez généralement 0 pour BLAH_NONE. :-) Merci d'avoir secoué ce souvenir!
Jamie

1
@Katu • la virgule superflue sur l'énumération finale est autorisée par la norme. Je n'aime pas ça, mais je sais déjà ce que Stroustrup me dirait ... "Vous ne l'aimez pas? Eh bien, n'hésitez pas à créer votre propre langage. Je l'ai fait."
Eljay

55

Pour les paresseux comme moi, voici une solution basée sur un modèle pour copier et coller:

template<class T> inline T operator~ (T a) { return (T)~(int)a; }
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
template<class T> inline T& operator|= (T& a, T b) { return (T&)((int&)a |= (int)b); }
template<class T> inline T& operator&= (T& a, T b) { return (T&)((int&)a &= (int)b); }
template<class T> inline T& operator^= (T& a, T b) { return (T&)((int&)a ^= (int)b); }

23
+1 La paresse est l'une des trois grandes vertus d'un programmeur: threevirtues.com
Pharap

10
C'est une très bonne solution, faites juste attention qu'elle fournira joyeusement des opérations au niveau du bit pour tout type. J'utilise quelque chose de similaire, mais avec l'ajout de traits identifiant les types auxquels je veux qu'il s'applique combiné avec un peu de magie enable_if.
Rai

@Rai: vous pouvez toujours le mettre dans un espace de noms et usingle cas échéant, comme rel_ops.
Yakov Galka

1
@ybungalobill, mais vous aurez toujours le même problème avec les opérations s'appliquant à n'importe quel type dans la portée de l'utilisation, qui correspondrait probablement à l'énumération? Je pense que les traits sont très probablement nécessaires.
Rai

19
N'utilisez pas ce code. Il ouvre la porte à N'IMPORTE QUELLE classe d'être exploitée par erreur. Le code utilise également un ancien style de cast qui ne passera pas par la compilation stricte de GCC shitalshah.com/p/… .
Shital Shah

44

Notez que si vous travaillez dans un environnement Windows, il existe une DEFINE_ENUM_FLAG_OPERATORSmacro définie dans winnt.h qui fait le travail pour vous. Donc dans ce cas, vous pouvez faire ceci:

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};
DEFINE_ENUM_FLAG_OPERATORS(AnimalFlags)

seahawk.flags = CanFly | EatsFish | Endangered;

44

Quel type est la variable seahawk.flags?

Dans C ++ standard, les énumérations ne sont pas de type sécurisé. Ce sont en fait des nombres entiers.

AnimalFlags ne doit PAS être le type de votre variable. Votre variable doit être int et l'erreur disparaîtra.

Il n'est pas nécessaire de mettre des valeurs hexadécimales comme certaines personnes l'ont suggéré. Ça ne fait aucune différence.

Les valeurs d'énumération SONT de type int par défaut. Vous pouvez donc sûrement les combiner au niveau du bit OU les combiner et les assembler et stocker le résultat dans un int.

Le type enum est un sous-ensemble restreint de int dont la valeur est l'une de ses valeurs énumérées. Par conséquent, lorsque vous créez une nouvelle valeur en dehors de cette plage, vous ne pouvez pas l'affecter sans transtyper en une variable de votre type enum.

Vous pouvez également modifier les types de valeur d'énumération si vous le souhaitez, mais cette question ne sert à rien.

ÉDITER: L'affiche a déclaré qu'ils étaient préoccupés par la sécurité des types et qu'ils ne veulent pas d'une valeur qui ne devrait pas exister à l'intérieur du type int.

Mais il serait de type unsafe de placer une valeur en dehors de la plage d'AnimalFlags dans une variable de type AnimalFlags.

Il existe un moyen sûr de vérifier les valeurs hors plage à l'intérieur du type int ...

int iFlags = HasClaws | CanFly;
//InvalidAnimalFlagMaxValue-1 gives you a value of all the bits 
// smaller than itself set to 1
//This check makes sure that no other bits are set.
assert(iFlags & ~(InvalidAnimalFlagMaxValue-1) == 0);

enum AnimalFlags {
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8,

    // put new enum values above here
    InvalidAnimalFlagMaxValue = 16
};

Ce qui précède ne vous empêche pas de mettre un indicateur non valide d'une énumération différente qui a la valeur 1, 2, 4 ou 8.

Si vous voulez une sécurité de type absolue, vous pouvez simplement créer un std :: set et stocker chaque drapeau à l'intérieur. Ce n'est pas peu encombrant, mais il est sûr de type et vous donne la même capacité qu'un bitflag int.

Remarque C ++ 0x: énumérations fortement typées

En C ++ 0x, vous pouvez enfin avoir des valeurs d'énumération de type safe ...

enum class AnimalFlags {
    CanFly = 2,
    HasClaws = 4
};

if(CanFly == 2) { }//Compiling error

4
Les valeurs d'énumération ne sont pas des entiers, mais elles se convertissent très facilement en entiers. Le type de HasClaws | CanFlyest un type entier, mais le type de HasClawsest AnimalFlags, pas un type entier.
Karu

1
Ah, mais que se passe-t-il si nous définissons la plage correcte de l'énumération non seulement pour les valeurs d'indicateur individuelles, mais aussi leurs combinaisons au niveau du bit. Alors la réponse d'Eidolon est correcte, et soutient que seules les combinaisons de l'énumération d'indicateur correcte peuvent être passées comme ce type.
Scott

3
@Scott: Il convient de noter que la norme C ++ définit la plage de valeurs valide d'une instance enum de cette façon. "pour une énumération où emin est le plus petit énumérateur et emax est le plus grand, les valeurs de l'énumération sont les valeurs de l'intervalle bmin à bmax, dé fi nies comme suit: Soit K 1 pour une représentation complémentaire à deux et 0 pour un un ' représentation du complément ou de la grandeur du signe. bmax est la plus petite valeur supérieure ou égale à max(|emin| − K, |emax|)et égale à (1u<<M) - 1, où Mest un entier non négatif. "
Ben Voigt

Pour ceux qui (comme moi) veulent juste quelque chose de pratique qui permet de manipuler les valeurs d'énumération au niveau du bit et qui ne semble pas trop moche avec les modèles et le casting de type, c'est une bonne solution; définissez simplement les variables pour qu'elles soient de type int.
Eric Sokolowsky

Notez également qu'en C ++, regular enum n'est pas techniquement par défaut intson type sous-jacent (soit avant C ++ 11 (IIRC), soit post-C ++ 11 lorsqu'aucun type sous-jacent n'est spécifié), bien que ce soit le enum class cas . Au lieu de cela, le type sous-jacent prend par défaut quelque chose d'assez grand pour représenter tous les énumérateurs, avec la seule vraie règle dure qu'il est seulement plus grand que ints'il doit explicitement l' être. Fondamentalement, le type sous-jacent est spécifié comme (paraphrasé) "tout ce qui fonctionne, mais c'est probablement à int moins que les énumérateurs ne soient trop gros pour int".
Justin Time - Réintègre Monica le

26

Je trouve la réponse actuellement acceptée par eidolon trop dangereuse. L'optimiseur du compilateur peut émettre des hypothèses sur les valeurs possibles de l'énumération et vous pouvez récupérer des déchets avec des valeurs non valides. Et généralement, personne ne veut définir toutes les permutations possibles dans les énumérations d'indicateurs.

Comme Brian R. Bondy le déclare ci-dessous, si vous utilisez C ++ 11 (ce que tout le monde devrait faire, c'est aussi bien), vous pouvez maintenant le faire plus facilement avec enum class:

enum class ObjectType : uint32_t
{
    ANIMAL = (1 << 0),
    VEGETABLE = (1 << 1),
    MINERAL = (1 << 2)
};


constexpr enum ObjectType operator |( const enum ObjectType selfValue, const enum ObjectType inValue )
{
    return (enum ObjectType)(uint32_t(selfValue) | uint32_t(inValue));
}

// ... add more operators here. 

Cela garantit une taille et une plage de valeurs stables en spécifiant un type pour l'énumération, empêche le downcasting automatique des énumérations en ints, etc. en utilisant enum class, et utilise constexprpour garantir que le code des opérateurs soit incorporé et donc aussi rapide que les nombres réguliers.

Pour les personnes bloquées avec des dialectes C ++ pré-11

Si j'étais coincé avec un compilateur qui ne prend pas en charge C ++ 11, j'irais avec l'encapsulation d'un type int dans une classe qui autorise alors uniquement l'utilisation d'opérateurs au niveau du bit et les types de cette énumération pour définir ses valeurs:

template<class ENUM,class UNDERLYING=typename std::underlying_type<ENUM>::type>
class SafeEnum
{
public:
    SafeEnum() : mFlags(0) {}
    SafeEnum( ENUM singleFlag ) : mFlags(singleFlag) {}
    SafeEnum( const SafeEnum& original ) : mFlags(original.mFlags) {}

    SafeEnum&   operator |=( ENUM addValue )    { mFlags |= addValue; return *this; }
    SafeEnum    operator |( ENUM addValue )     { SafeEnum  result(*this); result |= addValue; return result; }
    SafeEnum&   operator &=( ENUM maskValue )   { mFlags &= maskValue; return *this; }
    SafeEnum    operator &( ENUM maskValue )    { SafeEnum  result(*this); result &= maskValue; return result; }
    SafeEnum    operator ~()    { SafeEnum  result(*this); result.mFlags = ~result.mFlags; return result; }
    explicit operator bool()                    { return mFlags != 0; }

protected:
    UNDERLYING  mFlags;
};

Vous pouvez définir cela à peu près comme un enum + typedef normal:

enum TFlags_
{
    EFlagsNone  = 0,
    EFlagOne    = (1 << 0),
    EFlagTwo    = (1 << 1),
    EFlagThree  = (1 << 2),
    EFlagFour   = (1 << 3)
};

typedef SafeEnum<enum TFlags_>  TFlags;

Et l'utilisation est également similaire:

TFlags      myFlags;

myFlags |= EFlagTwo;
myFlags |= EFlagThree;

if( myFlags & EFlagTwo )
    std::cout << "flag 2 is set" << std::endl;
if( (myFlags & EFlagFour) == EFlagsNone )
    std::cout << "flag 4 is not set" << std::endl;

Et vous pouvez également remplacer le type sous-jacent pour les énumérations binaires stables (comme C ++ 11 enum foo : type) en utilisant le deuxième paramètre de modèle, ie typedef SafeEnum<enum TFlags_,uint8_t> TFlags;.

J'ai marqué le operator boolremplacement avec le explicitmot - clé de C ++ 11 pour l'empêcher de générer des conversions int, car ceux-ci pourraient entraîner la réduction des ensembles d'indicateurs en 0 ou 1 lors de leur écriture. Si vous ne pouvez pas utiliser C ++ 11, laissez cette surcharge et réécrivez le premier conditionnel dans l'exemple d'utilisation comme (myFlags & EFlagTwo) == EFlagTwo.


En guise de note, je recommanderais que l'opérateur exemple défini au début utilise std::underlying_typeau lieu de coder en dur un type spécifique, ou que le type sous-jacent soit fourni et utilisé comme alias de type au lieu de directement. De cette façon, les modifications du type sous-jacent se propageront automatiquement, au lieu de devoir être effectuées manuellement.
Justin Time - Réintègre Monica le

17

Le moyen le plus simple de le faire, comme indiqué ici , en utilisant le jeu de bits de la classe de bibliothèque standard .

Pour émuler la fonctionnalité C # d'une manière sûre pour le type, vous devez écrire un wrapper de modèle autour du jeu de bits, en remplaçant les arguments int par une énumération donnée en tant que paramètre de type au modèle. Quelque chose comme:

    template <class T, int N>
class FlagSet
{

    bitset<N> bits;

    FlagSet(T enumVal)
    {
        bits.set(enumVal);
    }

    // etc.
};

enum MyFlags
{
    FLAG_ONE,
    FLAG_TWO
};

FlagSet<MyFlags, 2> myFlag;

4
Regardez ceci pour un code plus complet: codereview.stackexchange.com/questions/96146/…
Shital Shah

11

À mon avis, aucune des réponses à ce jour n'est idéale. Pour être idéal, j'attendrais la solution:

  1. Soutenir le ==, !=, =, &, &=, |, |=et les ~opérateurs dans le sens conventionnel ( par exemple a & b)
  2. Soyez sûr du type, c'est-à-dire ne pas autoriser l'attribution de valeurs non énumérées telles que des littéraux ou des types entiers (sauf pour les combinaisons au niveau du bit de valeurs énumérées) ou autoriser l'attribution d'une variable enum à un type entier
  3. Autoriser les expressions telles que if (a & b)...
  4. Ne nécessite pas de macros maléfiques, de fonctionnalités spécifiques à l'implémentation ou d'autres hacks

La plupart des solutions à ce jour tombent sur les points 2 ou 3. WebDancer est la clôture à mon avis mais échoue au point 3 et doit être répétée pour chaque énumération.

Ma solution proposée est une version généralisée de WebDancer's qui aborde également le point 3:

#include <cstdint>
#include <type_traits>

template<typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
class auto_bool
{
    T val_;
public:
    constexpr auto_bool(T val) : val_(val) {}
    constexpr operator T() const { return val_; }
    constexpr explicit operator bool() const
    {
        return static_cast<std::underlying_type_t<T>>(val_) != 0;
    }
};

template <typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
constexpr auto_bool<T> operator&(T lhs, T rhs)
{
    return static_cast<T>(
        static_cast<typename std::underlying_type<T>::type>(lhs) &
        static_cast<typename std::underlying_type<T>::type>(rhs));
}

template <typename T = typename std::enable_if<std::is_enum<T>::value, T>::type>
constexpr T operator|(T lhs, T rhs)
{
    return static_cast<T>(
        static_cast<typename std::underlying_type<T>::type>(lhs) |
        static_cast<typename std::underlying_type<T>::type>(rhs));
}

enum class AnimalFlags : uint8_t 
{
    HasClaws = 1,
    CanFly = 2,
    EatsFish = 4,
    Endangered = 8
};

enum class PlantFlags : uint8_t
{
    HasLeaves = 1,
    HasFlowers = 2,
    HasFruit = 4,
    HasThorns = 8
};

int main()
{
    AnimalFlags seahawk = AnimalFlags::CanFly;        // Compiles, as expected
    AnimalFlags lion = AnimalFlags::HasClaws;         // Compiles, as expected
    PlantFlags rose = PlantFlags::HasFlowers;         // Compiles, as expected
//  rose = 1;                                         // Won't compile, as expected
    if (seahawk != lion) {}                           // Compiles, as expected
//  if (seahawk == rose) {}                           // Won't compile, as expected
//  seahawk = PlantFlags::HasThorns;                  // Won't compile, as expected
    seahawk = seahawk | AnimalFlags::EatsFish;        // Compiles, as expected
    lion = AnimalFlags::HasClaws |                    // Compiles, as expected
           AnimalFlags::Endangered;
//  int eagle = AnimalFlags::CanFly |                 // Won't compile, as expected
//              AnimalFlags::HasClaws;
//  int has_claws = seahawk & AnimalFlags::CanFly;    // Won't compile, as expected
    if (seahawk & AnimalFlags::CanFly) {}             // Compiles, as expected
    seahawk = seahawk & AnimalFlags::CanFly;          // Compiles, as expected

    return 0;
}

Cela crée des surcharges des opérateurs nécessaires mais utilise SFINAE pour les limiter aux types énumérés. Notez que dans un souci de concision, je n'ai pas défini tous les opérateurs mais le seul qui soit différent est le &. Les opérateurs sont actuellement globaux (c'est-à-dire s'appliquent à tous les types énumérés) mais cela pourrait être réduit soit en plaçant les surcharges dans un espace de noms (ce que je fais), soit en ajoutant des conditions SFINAE supplémentaires (peut-être en utilisant des types sous-jacents particuliers, ou des alias de type spécialement créés ). leunderlying_type_t s'agit d'une fonctionnalité C ++ 14 mais elle semble bien prise en charge et est facile à émuler pour C ++ 11 avec un simpletemplate<typename T> using underlying_type_t = underlying_type<T>::type;


Bien que la solution proposée fonctionne très bien, elle introduit également ce modèle pour les énumérations qui ne sont pas destinées à être traitées comme des indicateurs. C'est probablement la raison de l'utilisation de macros (diaboliques) comme DEFINE_ENUM_FLAG_OPERATORS de Microsoft.
WebDancer

@WebDancer, vous avez bien sûr raison, mais je l'ai déjà dit dans ma réponse. J'ai également suggéré deux façons de résoudre le problème - le mettre dans un espace de noms ou utiliser une condition SFINAE plus restrictive.
Trevor

Mon point est à moins que vous ne fassiez un espace de noms vraiment étroit (par exemple, l'espace de noms AllMyFlagEnums) ou que vous ayez une condition SFINAE qui d'une certaine manière ne sélectionne que quelques énumérations exactes, le code est cassé dans mon esprit. Plutôt que de risquer cela, je copie et colle un "modèle textuel" où je remplace simplement puis enum name, et parfois les macros "diaboliques". J'aimerais qu'il y ait un meilleur moyen.
WebDancer

Premièrement, cela ne posera un problème que si ailleurs dans votre code vous avez besoin de faire l'une des choses qu'il est censé arrêter, par exemple affecter un littéral, un entier ou un élément d'une autre énumération. Sinon, l'énumération modifiée se comporte comme une énumération régulière, par exemple les éléments n'ont pas nécessairement besoin d'être des puissances de deux et les opérations d'assignation, de comparaison et de bit fonctionnent normalement. Si vous devez vraiment attribuer des littéraux ou mélanger des énumérations, vous pouvez toujours effectuer un cast explicitement, avec l'avantage supplémentaire que votre intention sera plus claire. Il y a donc des chances qu'il ne soit pas nécessaire de réduire la portée.
Trevor

Deuxièmement, si même si vous avez besoin de réduire la portée, l'espace de noms n'a peut-être pas besoin d'être restreint, bien que cela dépende de ce que vous faites. Si vous travaillez sur une bibliothèque, vous avez peut-être déjà votre code qui dépend des énumérations dans un espace de noms, alors le code enum va simplement dans le même espace de noms. Si vous avez besoin du comportement enum pour une classe (peut-être souhaitez-vous utiliser les enums comme arguments de méthode ou comme variables membres de la classe), placez le code enum dans la classe pour le même effet. En bout de ligne, vous n'avez pas besoin d'envelopper un espace de noms uniquement autour des énumérations - bien que vous puissiez le faire.
Trevor

8

Le standard C ++ en parle explicitement, voir la section "17.5.2.1.3 Types de masque de bits":

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3485.pdf

Compte tenu de ce "modèle", vous obtenez:

enum AnimalFlags : unsigned int
{
    HasClaws = 1,
    CanFly = 2,
    EatsFish = 4,
    Endangered = 8
};

constexpr AnimalFlags operator|(AnimalFlags X, AnimalFlags Y) {
    return static_cast<AnimalFlags>(
        static_cast<unsigned int>(X) | static_cast<unsigned int>(Y));
}

AnimalFlags& operator|=(AnimalFlags& X, AnimalFlags Y) {
    X = X | Y; return X;
}

Et pareil pour les autres opérateurs. Notez également le "constexpr", il est nécessaire si vous voulez que le compilateur puisse exécuter les opérateurs lors de la compilation.

Si vous utilisez C ++ / CLI et que vous souhaitez attribuer des membres enum aux classes ref, vous devez utiliser des références de suivi à la place:

AnimalFlags% operator|=(AnimalFlags% X, AnimalFlags Y) {
    X = X | Y; return X;
}

REMARQUE: cet exemple n'est pas complet, voir la section "17.5.2.1.3 Types de masque de bits" pour un ensemble complet d'opérateurs.


6

Je me suis retrouvé à poser la même question et j'ai proposé une solution générique basée sur C ++ 11, similaire à celle de soru:

template <typename TENUM>
class FlagSet {

private:
    using TUNDER = typename std::underlying_type<TENUM>::type;
    std::bitset<std::numeric_limits<TUNDER>::max()> m_flags;

public:
    FlagSet() = default;

    template <typename... ARGS>
    FlagSet(TENUM f, ARGS... args) : FlagSet(args...)
    {   
        set(f);
    }   
    FlagSet& set(TENUM f)
    {   
        m_flags.set(static_cast<TUNDER>(f));
        return *this;
    }   
    bool test(TENUM f)
    {   
        return m_flags.test(static_cast<TUNDER>(f));
    }   
    FlagSet& operator|=(TENUM f)
    {   
        return set(f);
    }   
};

L'interface peut être améliorée au goût. Ensuite, il peut être utilisé comme ceci:

FlagSet<Flags> flags{Flags::FLAG_A, Flags::FLAG_C};
flags |= Flags::FLAG_D;

2
Regardez ceci pour un code meilleur et plus complet: codereview.stackexchange.com/questions/96146/…
Shital Shah

5
Sauf pour mon utilisation de numeric_limits, le code est presque le même. Je suppose que c'est un moyen courant d'avoir une classe d'énumération de type sûr. Je dirais que l'utilisation de numeric_limits est meilleure que de mettre un SENTINEL à la fin de chaque énumération.
Omair

1
C'est un énorme ensemble de bits!
Courses de légèreté en orbite

(potentiellement ...)
Courses de légèreté en orbite

5

Si votre compilateur ne prend pas encore en charge les énumérations fortement typées, vous pouvez consulter l' article suivant à partir de la source c ++:

Du résumé:

Cet article présente une solution au problème de la contrainte des opérations sur les bits pour
n'autoriser que les opérations sûres et légitimes et transforme toutes les manipulations de bits non valides en erreurs de compilation. Mieux encore, la syntaxe des opérations sur les bits reste inchangée et le code fonctionnant avec des bits n'a pas besoin d'être modifié, sauf peut-être pour corriger des erreurs qui n'étaient pas encore détectées.


5

J'utilise la macro suivante:

#define ENUM_FLAG_OPERATORS(T)                                                                                                                                            \
    inline T operator~ (T a) { return static_cast<T>( ~static_cast<std::underlying_type<T>::type>(a) ); }                                                                       \
    inline T operator| (T a, T b) { return static_cast<T>( static_cast<std::underlying_type<T>::type>(a) | static_cast<std::underlying_type<T>::type>(b) ); }                   \
    inline T operator& (T a, T b) { return static_cast<T>( static_cast<std::underlying_type<T>::type>(a) & static_cast<std::underlying_type<T>::type>(b) ); }                   \
    inline T operator^ (T a, T b) { return static_cast<T>( static_cast<std::underlying_type<T>::type>(a) ^ static_cast<std::underlying_type<T>::type>(b) ); }                   \
    inline T& operator|= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) |= static_cast<std::underlying_type<T>::type>(b) ); }   \
    inline T& operator&= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) &= static_cast<std::underlying_type<T>::type>(b) ); }   \
    inline T& operator^= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) ^= static_cast<std::underlying_type<T>::type>(b) ); }

Il est similaire à ceux mentionnés ci-dessus mais présente plusieurs améliorations:

  • Il est de type sécurisé (cela ne suppose pas que le type sous-jacent est un int)
  • Il ne nécessite pas de spécifier manuellement le type sous-jacent (contrairement à la réponse de @LunarEclipse)

Il doit inclure type_traits:

#include <type_traits>

4

Je voudrais développer la réponse d'Uliwitness , en corrigeant son code pour C ++ 98 et en utilisant l' idiome Safe Bool , faute de std::underlying_type<>modèle et deexplicit mot clé dans les versions C ++ inférieures à C ++ 11.

Je l'ai également modifié pour que les valeurs d'énumération puissent être séquentielles sans aucune affectation explicite, afin que vous puissiez avoir

enum AnimalFlags_
{
    HasClaws,
    CanFly,
    EatsFish,
    Endangered
};
typedef FlagsEnum<AnimalFlags_> AnimalFlags;

seahawk.flags = AnimalFlags() | CanFly | EatsFish | Endangered;

Vous pouvez ensuite obtenir la valeur des indicateurs bruts avec

seahawk.flags.value();

Voici le code.

template <typename EnumType, typename Underlying = int>
class FlagsEnum
{
    typedef Underlying FlagsEnum::* RestrictedBool;

public:
    FlagsEnum() : m_flags(Underlying()) {}

    FlagsEnum(EnumType singleFlag):
        m_flags(1 << singleFlag)
    {}

    FlagsEnum(const FlagsEnum& original):
        m_flags(original.m_flags)
    {}

    FlagsEnum& operator |=(const FlagsEnum& f) {
        m_flags |= f.m_flags;
        return *this;
    }

    FlagsEnum& operator &=(const FlagsEnum& f) {
        m_flags &= f.m_flags;
        return *this;
    }

    friend FlagsEnum operator |(const FlagsEnum& f1, const FlagsEnum& f2) {
        return FlagsEnum(f1) |= f2;
    }

    friend FlagsEnum operator &(const FlagsEnum& f1, const FlagsEnum& f2) {
        return FlagsEnum(f1) &= f2;
    }

    FlagsEnum operator ~() const {
        FlagsEnum result(*this);
        result.m_flags = ~result.m_flags;
        return result;
    }

    operator RestrictedBool() const {
        return m_flags ? &FlagsEnum::m_flags : 0;
    }

    Underlying value() const {
        return m_flags;
    }

protected:
    Underlying  m_flags;
};

3

Voici une option pour les masques de bits si vous n'avez pas réellement d'utilisation des valeurs d'énumération individuelles (par exemple, vous n'avez pas besoin de les désactiver) ... et si vous n'êtes pas préoccupé par le maintien de la compatibilité binaire, c'est-à-dire: vous peu importe où vivent vos morceaux ... ce que vous êtes probablement. De plus, vous feriez mieux de ne pas vous préoccuper trop de la portée et du contrôle d'accès. Hmmm, les enums ont de belles propriétés pour les champs de bits ... je me demande si quelqu'un a déjà essayé ça :)

struct AnimalProperties
{
    bool HasClaws : 1;
    bool CanFly : 1;
    bool EatsFish : 1;
    bool Endangered : 1;
};

union AnimalDescription
{
    AnimalProperties Properties;
    int Flags;
};

void TestUnionFlags()
{
    AnimalDescription propertiesA;
    propertiesA.Properties.CanFly = true;

    AnimalDescription propertiesB = propertiesA;
    propertiesB.Properties.EatsFish = true;

    if( propertiesA.Flags == propertiesB.Flags )
    {
        cout << "Life is terrible :(";
    }
    else
    {
        cout << "Life is great!";
    }

    AnimalDescription propertiesC = propertiesA;
    if( propertiesA.Flags == propertiesC.Flags )
    {
        cout << "Life is great!";
    }
    else
    {
        cout << "Life is terrible :(";
    }
}

Nous pouvons voir que la vie est belle, nous avons nos valeurs discrètes, et nous avons un bel intérêt pour & et | à notre coeur content, qui a toujours le contexte de ce que signifient ses morceaux. Tout est cohérent et prévisible ... pour moi ... tant que je continue à utiliser le compilateur VC ++ de Microsoft avec la mise à jour 3 sur Win10 x64 et que je ne touche pas les drapeaux de mon compilateur :)

Même si tout va bien ... nous avons maintenant un certain contexte quant à la signification des drapeaux, car c'est dans une union avec le champ de bits dans le terrible monde réel où votre programme peut être responsable de plus d'une seule tâche discrète que vous pourriez écrasez toujours accidentellement (assez facilement) deux champs de drapeaux de différents syndicats ensemble (par exemple, AnimalProperties et ObjectProperties, car ils sont tous les deux intimes), mélangeant tous vos bits, ce qui est un horrible bug à retracer ... et comment je sais beaucoup de personnes sur ce post ne travaillent pas très souvent avec des masques bitmap, car leur construction est facile et leur maintenance est difficile.

class AnimalDefinition {
public:
    static AnimalDefinition *GetAnimalDefinition( AnimalFlags flags );   //A little too obvious for my taste... NEXT!
    static AnimalDefinition *GetAnimalDefinition( AnimalProperties properties );   //Oh I see how to use this! BORING, NEXT!
    static AnimalDefinition *GetAnimalDefinition( int flags ); //hmm, wish I could see how to construct a valid "flags" int without CrossFingers+Ctrl+Shift+F("Animal*"). Maybe just hard-code 16 or something?

    AnimalFlags animalFlags;  //Well this is *way* too hard to break unintentionally, screw this!
    int flags; //PERFECT! Nothing will ever go wrong here... 
    //wait, what values are used for this particular flags field? Is this AnimalFlags or ObjectFlags? Or is it RuntimePlatformFlags? Does it matter? Where's the documentation? 
    //Well luckily anyone in the code base and get confused and destroy the whole program! At least I don't need to static_cast anymore, phew!

    private:
    AnimalDescription m_description; //Oh I know what this is. All of the mystery and excitement of life has been stolen away :(
}

Donc, vous rendez votre déclaration d'union privée pour empêcher l'accès direct à "Flags", et devez ajouter des getters / setters et des surcharges d'opérateurs, puis créer une macro pour tout cela, et vous êtes fondamentalement de retour là où vous avez commencé lorsque vous avez essayé de faites cela avec un Enum.

Malheureusement, si vous voulez que votre code soit portable, je ne pense pas qu'il y ait moyen de A) garantir la disposition des bits ou B) déterminer la disposition des bits au moment de la compilation (afin que vous puissiez le suivre et au moins corriger les changements à travers versions / plates-formes, etc.) Offset dans une structure avec des champs de bits

Au moment de l'exécution, vous pouvez jouer des tours en définissant les champs et en XORing les indicateurs pour voir quels bits ont changé, cela me semble assez merdique si les versets ont une solution 100% cohérente, indépendante de la plate-forme et complètement déterministe, c'est-à-dire: un ENUM.

TL; DR: N'écoutez pas les haineux. C ++ n'est pas l'anglais. Ce n'est pas parce que la définition littérale d'un mot-clé abrégé hérité de C peut ne pas correspondre à votre utilisation que vous ne devriez pas l'utiliser lorsque le C et définition C ++ du mot clé inclut absolument votre cas d'utilisation. Vous pouvez également utiliser des structures pour modéliser des choses autres que des structures et des classes pour des choses autres que l'école et la caste sociale. Vous pouvez utiliser float pour les valeurs mises à la terre. Vous pouvez utiliser char pour les variables qui ne sont ni non brûlées ni une personne dans un roman, une pièce de théâtre ou un film. Tout programmeur qui va au dictionnaire pour déterminer la signification d'un mot-clé avant la spécification de la langue est un ... eh bien, je vais garder ma langue là-bas.

Si vous voulez que votre code soit modelé sur le langage parlé, vous feriez mieux d'écrire en Objective-C, qui utilise également fortement les énumérations pour les champs de bits.


3

Uniquement du sucre syntaxique. Aucune métadonnée supplémentaire.

namespace UserRole // grupy
{ 
    constexpr uint8_t dea = 1;
    constexpr uint8_t red = 2;
    constexpr uint8_t stu = 4;
    constexpr uint8_t kie = 8;
    constexpr uint8_t adm = 16;
    constexpr uint8_t mas = 32;
}

Les opérateurs de drapeau sur le type intégral fonctionnent simplement.


IMHO c'est la meilleure réponse. Syntaxe client claire et simple. J'utiliserais simplement "const int" plutôt que "constexpr uint8_t", mais le concept est le même.
yoyo

(désolé, "constexpr int")
yoyo

3

Actuellement, il n'y a pas de prise en charge du langage pour les indicateurs enum, les classes Meta pourraient intrinsèquement ajouter cette fonctionnalité si elle faisait jamais partie du standard c ++.

Ma solution serait de créer des fonctions de modèle instanciées enum uniquement en ajoutant la prise en charge des opérations bit à bit de type sécurisé pour la classe enum en utilisant son type sous-jacent:

Fichier: EnumClassBitwise.h

#pragma once
#ifndef _ENUM_CLASS_BITWISE_H_
#define _ENUM_CLASS_BITWISE_H_

#include <type_traits>

//unary ~operator    
template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum& operator~ (Enum& val)
{
    val = static_cast<Enum>(~static_cast<std::underlying_type_t<Enum>>(val));
    return val;
}

// & operator
template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum operator& (Enum lhs, Enum rhs)
{
    return static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) & static_cast<std::underlying_type_t<Enum>>(rhs));
}

// &= operator
template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum operator&= (Enum& lhs, Enum rhs)
{
    lhs = static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) & static_cast<std::underlying_type_t<Enum>>(rhs));
    return lhs;
}

//| operator

template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum operator| (Enum lhs, Enum rhs)
{
    return static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) | static_cast<std::underlying_type_t<Enum>>(rhs));
}
//|= operator

template <typename Enum, typename std::enable_if_t<std::is_enum<Enum>::value, int> = 0>
constexpr inline Enum& operator|= (Enum& lhs, Enum rhs)
{
    lhs = static_cast<Enum>(static_cast<std::underlying_type_t<Enum>>(lhs) | static_cast<std::underlying_type_t<Enum>>(rhs));
    return lhs;
}

#endif // _ENUM_CLASS_BITWISE_H_

Pour plus de commodité et pour réduire les erreurs, vous voudrez peut-être encapsuler vos opérations d'indicateurs de bits pour les énumérations et pour les entiers également:

Fichier: BitFlags.h

#pragma once
#ifndef _BIT_FLAGS_H_
#define _BIT_FLAGS_H_

#include "EnumClassBitwise.h"

 template<typename T>
 class BitFlags
 {
 public:

     constexpr inline BitFlags() = default;
     constexpr inline BitFlags(T value) { mValue = value; }
     constexpr inline BitFlags operator| (T rhs) const { return mValue | rhs; }
     constexpr inline BitFlags operator& (T rhs) const { return mValue & rhs; }
     constexpr inline BitFlags operator~ () const { return ~mValue; }
     constexpr inline operator T() const { return mValue; }
     constexpr inline BitFlags& operator|=(T rhs) { mValue |= rhs; return *this; }
     constexpr inline BitFlags& operator&=(T rhs) { mValue &= rhs; return *this; }
     constexpr inline bool test(T rhs) const { return (mValue & rhs) == rhs; }
     constexpr inline void set(T rhs) { mValue |= rhs; }
     constexpr inline void clear(T rhs) { mValue &= ~rhs; }

 private:
     T mValue;
 };
#endif //#define _BIT_FLAGS_H_

Utilisation possible:

#include <cstdint>
#include <BitFlags.h>
void main()
{
    enum class Options : uint32_t
    { 
          NoOption = 0 << 0
        , Option1  = 1 << 0
        , Option2  = 1 << 1
        , Option3  = 1 << 2
        , Option4  = 1 << 3
    };

    const uint32_t Option1 = 1 << 0;
    const uint32_t Option2 = 1 << 1;
    const uint32_t Option3 = 1 << 2;
    const uint32_t Option4 = 1 << 3;

   //Enum BitFlags
    BitFlags<Options> optionsEnum(Options::NoOption);
    optionsEnum.set(Options::Option1 | Options::Option3);

   //Standard integer BitFlags
    BitFlags<uint32_t> optionsUint32(0);
    optionsUint32.set(Option1 | Option3); 

    return 0;
}

3

@Xaqq a fourni une très bonne façon d'utiliser les indicateurs enum ici par une flag_setclasse.

J'ai publié le code dans GitHub , l'utilisation est la suivante:

#include "flag_set.hpp"

enum class AnimalFlags : uint8_t {
    HAS_CLAWS,
    CAN_FLY,
    EATS_FISH,
    ENDANGERED,
    _
};

int main()
{
    flag_set<AnimalFlags> seahawkFlags(AnimalFlags::HAS_CLAWS
                                       | AnimalFlags::EATS_FISH
                                       | AnimalFlags::ENDANGERED);

    if (seahawkFlags & AnimalFlags::ENDANGERED)
        cout << "Seahawk is endangered";
}

2

Vous confondez objets et collections d'objets. Plus précisément, vous confondez les indicateurs binaires avec des ensembles d'indicateurs binaires. Une solution appropriée ressemblerait à ceci:

// These are individual flags
enum AnimalFlag // Flag, not Flags
{
    HasClaws = 0,
    CanFly,
    EatsFish,
    Endangered
};

class AnimalFlagSet
{
    int m_Flags;

  public:

    AnimalFlagSet() : m_Flags(0) { }

    void Set( AnimalFlag flag ) { m_Flags |= (1 << flag); }

    void Clear( AnimalFlag flag ) { m_Flags &= ~ (1 << flag); }

    bool Get( AnimalFlag flag ) const { return (m_Flags >> flag) & 1; }

};

2

Voici ma solution sans avoir besoin de surcharge ou de casting:

namespace EFoobar
{
    enum
    {
        FB_A    = 0x1,
        FB_B    = 0x2,
        FB_C    = 0x4,
    };
    typedef long Flags;
}

void Foobar(EFoobar::Flags flags)
{
    if (flags & EFoobar::FB_A)
        // do sth
        ;
    if (flags & EFoobar::FB_B)
        // do sth
        ;
}

void ExampleUsage()
{
    Foobar(EFoobar::FB_A | EFoobar::FB_B);
    EFoobar::Flags otherflags = 0;
    otherflags|= EFoobar::FB_B;
    otherflags&= ~EFoobar::FB_B;
    Foobar(otherflags);
}

Je pense que c'est correct, car nous identifions de toute façon les énumérations et les entiers (non fortement typés).

Juste comme une note latérale (plus longue), si vous

  • veulent utiliser des énumérations fortement typées et
  • pas besoin de jouer avec vos drapeaux
  • la performance n'est pas un problème

Je proposerais ceci:

#include <set>

enum class EFoobarFlags
{
    FB_A = 1,
    FB_B,
    FB_C,
};

void Foobar(const std::set<EFoobarFlags>& flags)
{
    if (flags.find(EFoobarFlags::FB_A) != flags.end())
        // do sth
        ;
    if (flags.find(EFoobarFlags::FB_B) != flags.end())
        // do sth
        ;
}

void ExampleUsage()
{
    Foobar({EFoobarFlags::FB_A, EFoobarFlags::FB_B});
    std::set<EFoobarFlags> otherflags{};
    otherflags.insert(EFoobarFlags::FB_B);
    otherflags.erase(EFoobarFlags::FB_B);
    Foobar(otherflags);
}

en utilisant les listes d'initialiseurs C ++ 11 et enum class.


En passant, je ne recommanderais pas du tout les énumérations pour les drapeaux. Raison simple: les combinaisons d'indicateurs ne sont plus des éléments de l'énumération. Donc, cela semble tout à fait inapproprié. Sinon, j'utiliserais un à l' using Flags = unsigned longintérieur d'un espace de noms ou d'une structure contenant les valeurs d'indicateur elles-mêmes comme /*static*/ const Flags XY = 0x01et ainsi de suite.
yau

1

Comme ci-dessus (Kai) ou procédez comme suit. Vraiment les énumérations sont des "énumérations", ce que vous voulez faire est d'avoir un ensemble, donc vous devriez vraiment utiliser stl :: set

enum AnimalFlags
{
    HasClaws = 1,
    CanFly =2,
    EatsFish = 4,
    Endangered = 8
};

int main(void)
{
    AnimalFlags seahawk;
    //seahawk= CanFly | EatsFish | Endangered;
    seahawk= static_cast<AnimalFlags>(CanFly | EatsFish | Endangered);
}

1

Peut-être comme NS_OPTIONS d'Objective-C.

#define ENUM(T1, T2) \
enum class T1 : T2; \
inline T1 operator~ (T1 a) { return (T1)~(int)a; } \
inline T1 operator| (T1 a, T1 b) { return static_cast<T1>((static_cast<T2>(a) | static_cast<T2>(b))); } \
inline T1 operator& (T1 a, T1 b) { return static_cast<T1>((static_cast<T2>(a) & static_cast<T2>(b))); } \
inline T1 operator^ (T1 a, T1 b) { return static_cast<T1>((static_cast<T2>(a) ^ static_cast<T2>(b))); } \
inline T1& operator|= (T1& a, T1 b) { return reinterpret_cast<T1&>((reinterpret_cast<T2&>(a) |= static_cast<T2>(b))); } \
inline T1& operator&= (T1& a, T1 b) { return reinterpret_cast<T1&>((reinterpret_cast<T2&>(a) &= static_cast<T2>(b))); } \
inline T1& operator^= (T1& a, T1 b) { return reinterpret_cast<T1&>((reinterpret_cast<T2&>(a) ^= static_cast<T2>(b))); } \
enum class T1 : T2

ENUM(Options, short) {
    FIRST  = 1 << 0,
    SECOND = 1 << 1,
    THIRD  = 1 << 2,
    FOURTH = 1 << 3
};

auto options = Options::FIRST | Options::SECOND;
options |= Options::THIRD;
if ((options & Options::SECOND) == Options::SECOND)
    cout << "Contains second option." << endl;
if ((options & Options::THIRD) == Options::THIRD)
    cout << "Contains third option." << endl;
return 0;

// Output:
// Contains second option. 
// Contains third option.

Pouvez-vous expliquer pourquoi votre réponse est la meilleure solution? Il y a plusieurs autres réponses qui ont répondu à cette question, veuillez donc inclure quelques informations pour différencier la vôtre.
trevorp
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.