Question

Dans un projet C ++ sur lequel je travaille, j'ai un type de valeur indicateur qui peut avoir quatre valeurs. Ces quatre drapeaux peuvent être combinés. Les drapeaux décrivent les enregistrements de la base de données et peuvent être:

  • nouvel enregistrement
  • enregistrement supprimé
  • enregistrement modifié
  • enregistrement existant

Maintenant, pour chaque enregistrement, je souhaite conserver cet attribut afin de pouvoir utiliser une énumération:

enum { xNew, xDeleted, xModified, xExisting }

Cependant, à d’autres endroits du code, je dois sélectionner les enregistrements visibles pour l’utilisateur. Par conséquent, j'aimerais pouvoir le transmettre sous forme de paramètre unique, tel que:

showRecords(xNew | xDeleted);

Il semble donc que j'ai trois approches possibles:

#define X_NEW      0x01
#define X_DELETED  0x02
#define X_MODIFIED 0x04
#define X_EXISTING 0x08

ou

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

ou

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

L'espace requis est important (octet vs int) mais n'est pas crucial. Avec define, je perds la sécurité de type, et avec enum je perds un peu d’espace (nombres entiers) et je dois probablement transtyper lorsque je souhaite effectuer une opération au niveau des bits. Avec const, je pense aussi que je perds la sécurité de type car un uint8 aléatoire pourrait entrer par erreur.

Existe-t-il une autre méthode plus propre?

Si non, qu'utiliseriez-vous et pourquoi?

P.S. Le reste du code est un C ++ moderne et épuré, sans #define s, et j'ai utilisé des espaces de noms et des modèles dans quelques espaces, donc ils ne sont pas hors de question non plus.

Était-ce utile?

La solution

Combinez les stratégies pour réduire les inconvénients d’une approche unique. Comme je travaille dans des systèmes embarqués, la solution suivante est basée sur le fait que les opérateurs entiers et au niveau des bits sont rapides, mémoire faible & Amp; faible utilisation du flash.

Placez l'énumération dans un espace de noms pour empêcher les constantes de polluer l'espace de noms global.

namespace RecordType {

Une enum déclare et définit un temps de compilation vérifié, tapé. Utilisez toujours la vérification du type de temps de compilation pour vous assurer que le type correct est attribué aux arguments et aux variables. Le typedef en C ++ n'est pas nécessaire.

enum TRecordType { xNew = 1, xDeleted = 2, xModified = 4, xExisting = 8,

Créez un autre membre pour un état non valide. Cela peut être utile en tant que code d'erreur. Par exemple, lorsque vous souhaitez renvoyer l'état mais que l'opération d'E / S échoue. C'est également utile pour le débogage; utilisez-le dans les listes d'initialisation et les destructeurs pour savoir si la valeur de la variable doit être utilisée.

xInvalid = 16 };

Considérez que vous avez deux objectifs pour ce type. Pour suivre l'état actuel d'un enregistrement et créer un masque pour sélectionner des enregistrements dans certains états. Créez une fonction en ligne pour tester si la valeur du type est valide pour votre objectif. comme un marqueur d'état vs un masque d'état. Cela capturera les bogues, car typedef est simplement un int et une valeur telle que 0xDEADBEEF peut figurer dans votre variable via des variables non initialisées ou mal dirigées.

inline bool IsValidState( TRecordType v) {
    switch(v) { case xNew: case xDeleted: case xModified: case xExisting: return true; }
    return false;
}

 inline bool IsValidMask( TRecordType v) {
    return v >= xNew  && v < xInvalid ;
}

Ajoutez une directive using si vous souhaitez utiliser souvent ce type.

using RecordType ::TRecordType ;

Les fonctions de vérification de valeur sont utiles dans les assertions pour intercepter les valeurs incorrectes dès leur utilisation. Plus vite vous attrapez un bogue lorsque vous courez, moins il peut faire de dégâts.

Voici quelques exemples pour mettre tout cela ensemble.

void showRecords(TRecordType mask) {
    assert(RecordType::IsValidMask(mask));
    // do stuff;
}

void wombleRecord(TRecord rec, TRecordType state) {
    assert(RecordType::IsValidState(state));
    if (RecordType ::xNew) {
    // ...
} in runtime

TRecordType updateRecord(TRecord rec, TRecordType newstate) {
    assert(RecordType::IsValidState(newstate));
    //...
    if (! access_was_successful) return RecordType ::xInvalid;
    return newstate;
}

Le seul moyen de garantir la sécurité des valeurs correctes consiste à utiliser une classe dédiée avec des surcharges d’opérateurs, qui reste comme un exercice pour un autre lecteur.

Autres conseils

Oubliez les définitions

Ils pollueront votre code.

champs de bits?

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

Ne l'utilisez jamais . Vous êtes plus préoccupé par la vitesse que par l’économie de 4 pouces. L'utilisation de champs de bits est en réalité plus lente que l'accès à tout autre type.

Cependant, les membres de bits dans les structures ont des inconvénients pratiques. Premièrement, l'ordre des bits en mémoire varie d'un compilateur à l'autre. En outre, de nombreux compilateurs courants génèrent un code peu efficace pour la lecture et l’écriture des membres de bits , et il existe des problèmes de sécurité des threads potentiellement graves, liés aux champs de bits (en particulier sur les systèmes multiprocesseurs) en raison de: le fait que la plupart des machines ne peuvent pas manipuler des ensembles arbitraires de bits en mémoire, mais doivent plutôt charger et stocker des mots entiers. Par exemple, ce qui suit ne serait pas thread-safe, malgré l'utilisation d'un mutex

Source: http://fr.wikipedia.org/wiki/Bit_field :

Et si vous avez besoin de plus de raisons pour ne pas utiliser des champs de bits, peut-être Raymond Chen saura vous convaincre dans son The Old New Thing Message: L'analyse coûts-avantages de bitfields pour une collection de booléens à l'adresse http: // blogs.msdn.com/oldnewthing/archive/2008/11/26/9143050.aspx

const int?

namespace RecordType {
    static const uint8 xNew = 1;
    static const uint8 xDeleted = 2;
    static const uint8 xModified = 4;
    static const uint8 xExisting = 8;
}

Les placer dans un espace de noms est cool. S'ils sont déclarés dans votre fichier CPP ou dans le fichier d'en-tête, leurs valeurs seront en ligne. Vous pourrez utiliser ces valeurs, mais cela augmentera légèrement le couplage.

Ah, oui: supprimez le mot clé statique . static est obsolète en C ++ lorsqu'il est utilisé comme vous le faites, et si uint8 est un type de construction, vous n'en aurez pas besoin pour le déclarer dans un en-tête inclus par plusieurs sources du même module. Au final, le code devrait être:

namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

Le problème de cette approche est que votre code connaît la valeur de vos constantes, ce qui augmente légèrement le couplage.

énumération

Identique à const int, avec une frappe un peu plus forte.

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

Cependant, ils continuent de polluer l’espace de noms global. Au fait ... Supprimez le typedef . Vous travaillez en C ++. Ces types d’énumérations et de structures polluent le code plus que toute autre chose.

Le résultat est un peu:

enum RecordType { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;

void doSomething(RecordType p_eMyEnum)
{
   if(p_eMyEnum == xNew)
   {
       // etc.
   }
}

Comme vous le voyez, votre enum pollue l’espace de noms global. Si vous mettez cette énumération dans un espace de noms, vous obtiendrez quelque chose comme:

namespace RecordType {
   enum Value { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } ;
}

void doSomething(RecordType::Value p_eMyEnum)
{
   if(p_eMyEnum == RecordType::xNew)
   {
       // etc.
   }
}

extern const int?

Si vous souhaitez diminuer le couplage (c.-à-d. pouvoir masquer les valeurs des constantes et les modifier à votre guise sans avoir besoin d'une recompilation complète), vous pouvez déclarer l'ints en tant qu'extern dans l'en-tête et en tant que constante dans. le fichier CPP, comme dans l'exemple suivant:

// Header.hpp
namespace RecordType {
    extern const uint8 xNew ;
    extern const uint8 xDeleted ;
    extern const uint8 xModified ;
    extern const uint8 xExisting ;
}

Et:

// Source.hpp
namespace RecordType {
    const uint8 xNew = 1;
    const uint8 xDeleted = 2;
    const uint8 xModified = 4;
    const uint8 xExisting = 8;
}

Cependant, vous ne pourrez pas utiliser le commutateur sur ces constantes. Alors à la fin, choisissez votre poison ... :-p

Avez-vous exclu std :: bitset? Des ensembles de drapeaux est ce que c'est. Faire

typedef std::bitset<4> RecordType;

puis

static const RecordType xNew(1);
static const RecordType xDeleted(2);
static const RecordType xModified(4);
static const RecordType xExisting(8);

Comme il y a beaucoup de surcharges d'opérateurs pour le jeu de bits, vous pouvez maintenant le faire

.
RecordType rt = whatever;      // unsigned long or RecordType expression
rt |= xNew;                    // set 
rt &= ~xDeleted;               // clear 
if ((rt & xModified) != 0) ... // test

Ou quelque chose de très similaire à cela - je vous serais reconnaissant de faire des corrections car je n’ai pas testé cela. Vous pouvez également faire référence aux bits par index, mais il est généralement préférable de ne définir qu'un seul ensemble de constantes. Les constantes RecordType sont probablement plus utiles.

En supposant que vous ayez éliminé le jeu de bits, je vote pour la énumération .

Je n'achète pas que lancer des enums est un sérieux inconvénient - OK, donc c'est un peu bruyant, et attribuer une valeur hors de portée à une enum est un comportement indéfini, il est donc théoriquement possible de se tirer une balle dans le pied quelques implémentations C ++ inhabituelles. Mais si vous ne le faites que lorsque cela est nécessaire (c'est-à-dire lorsque vous passez d'int en enum iirc), c'est un code parfaitement normal que les gens ont déjà vu.

Je suis également sceptique quant aux coûts d'espace de l'énum. Les variables et paramètres uint8 n'utiliseront probablement pas moins de pile que ints, donc seul le stockage dans les classes est important. Il y a des cas où le regroupement de plusieurs octets dans une structure va gagner (dans ce cas, vous pouvez transtyper des énumérations dans et hors de la mémoire uint8), mais normalement le rembourrage annulera l'avantage de toute façon.

Ainsi, l’énumération n’a aucun inconvénient par rapport aux autres, et offre un avantage supplémentaire en termes de sécurité de type (vous ne pouvez pas affecter de valeur entière aléatoire sans transtypage explicite) et de moyens de référence propres à tout.

De préférence, je mettrais aussi le " = 2 " au fait, au fait. Ce n'est pas nécessaire, mais un & "Principe de moindre étonnement &"; suggère que les quatre définitions soient identiques.

Voici quelques articles sur const vs macros vs enums:

Constantes symboliques
Constantes d'énumération contre constantes Objets

Je pense que vous devriez éviter les macros, surtout que vous avez écrit la majeure partie de votre nouveau code en C ++ moderne.

Si possible, n'utilisez PAS de macros. Ils ne sont pas trop admirés en ce qui concerne le C ++ moderne.

Les énumérations seraient plus appropriées car elles fournissent & "signifiant aux identificateurs &"; ainsi que le type de sécurité. Vous pouvez clairement indiquer & Quot; xDeleted & Quot; est de " RecordType " et qui représentent & "le type d'un enregistrement &"; (wow!) même après des années. Les consts auraient besoin de commentaires à ce sujet, mais aussi de monter et descendre dans le code.

  

Avec définit, je perds la sécurité de type

Pas nécessairement ...

// signed defines
#define X_NEW      0x01u
#define X_NEW      (unsigned(0x01))  // if you find this more readable...
  

et avec enum je perds de l'espace (nombres entiers)

Pas nécessairement - mais vous devez être explicite aux points de stockage ...

struct X
{
    RecordType recordType : 4;  // use exactly 4 bits...
    RecordType recordType2 : 4;  // use another 4 bits, typically in the same byte
    // of course, the overall record size may still be padded...
};
  

et probablement jeté quand je veux faire l'opération au niveau du bit.

Vous pouvez créer des opérateurs pour vous simplifier la vie:

RecordType operator|(RecordType lhs, RecordType rhs)
{
    return RecordType((unsigned)lhs | (unsigned)rhs);
}
  

Avec const, je pense aussi que je perds la sécurité de type car un uint8 aléatoire pourrait entrer par erreur.

La même chose peut arriver avec n'importe lequel de ces mécanismes: les vérifications de plage et de valeur sont normalement orthogonales au type safety (bien que les types définis par l'utilisateur - c'est-à-dire vos propres classes - puissent appliquer des & "invariants &" à propos de leurs données). Avec les énumérations, le compilateur est libre de choisir un type plus grand pour héberger les valeurs, et une variable d’énumification non initialisée, corrompue ou mal définie peut toujours interpréter son modèle de bits comme un nombre inattendu - en comparant des inégalités les identificateurs d'énumération, toute combinaison d'entre eux, et 0.

  

Y a-t-il un autre moyen plus propre? / Sinon, qu'utiliseriez-vous et pourquoi?

Eh bien, au bout du compte, la ou des énumérations de bits au style C, qui a fait ses preuves, fonctionne plutôt bien une fois que vous avez des champs de bits et des opérateurs personnalisés dans l’image. Vous pouvez encore améliorer votre robustesse avec certaines fonctions de validation personnalisées et certaines assertions, comme dans la réponse de mat_geek; techniques souvent également applicables à la manipulation de chaîne, int, valeurs doubles, etc.

Vous pouvez dire que c'est & "plus propre &";

enum RecordType { New, Deleted, Modified, Existing };

showRecords([](RecordType r) { return r == New || r == Deleted; });

Je suis indifférent: les bits de données sont plus serrés mais le code grossit de manière significative ... dépend du nombre d'objets que vous avez, et les lamdbas - aussi beaux soient-ils - sont encore plus confus et difficiles à obtenir que les OR .

BTW / - l'argument relatif à l'IMHO, qui est plutôt faible, sur la sécurité des fils le partage d'un mutex à travers les champs de bits est une pratique plus probable, même s'ils ne sont pas conscients de leur emballage (les mutex sont des membres de données relativement volumineux - je dois vraiment être soucieux de la performance pour envisager d'avoir plusieurs mutex sur les membres d'un même objet, et je regarde attentivement. assez pour remarquer qu'ils étaient des champs de bits). Tout type de taille de sous-mot peut avoir le même problème (par exemple, un uint8_t). Quoi qu’il en soit, vous pouvez essayer des opérations de style atomique de comparaison et échange si vous êtes désespérément à la recherche d’une concurrence accrue.

Même si vous devez utiliser 4 octets pour stocker une énumération (je ne connais pas très bien le C ++ - je sais que vous pouvez spécifier le type sous-jacent en C #), cela vaut toujours la peine - utilisez des énumérations.

De nos jours, les serveurs dotés de Go de mémoire n'importent généralement pas entre 4 octets et 1 octet de mémoire au niveau de l'application. Bien sûr, si dans votre situation particulière, l’utilisation de la mémoire est aussi importante (et que vous ne pouvez pas obliger le C ++ à utiliser un octet pour sauvegarder l’énum), vous pouvez alors envisager la route «statique».

À la fin de la journée, vous devez vous poser la question suivante: vaut-il la peine de vouloir utiliser 'static const' pour les 3 octets d'économie de mémoire de votre structure de données?

Autre chose à garder à l'esprit - IIRC, sur x86, les structures de données sont alignées sur 4 octets. Par conséquent, à moins que vous n'ayez un certain nombre d'éléments d'une largeur en octets dans votre structure 'record', cela n'a peut-être pas d'importance. Testez-le et assurez-vous que c'est le cas avant de faire un compromis entre maintenabilité et performances / espace.

Si vous voulez la sécurité de type des classes, avec la commodité de la syntaxe d'énumération et du contrôle de bits, considérez Étiquettes de sécurité en C ++ . J'ai travaillé avec l'auteur et il est plutôt intelligent.

Attention, cependant. Au final, ce paquet utilise des macros modèles et !

Avez-vous réellement besoin de transmettre les valeurs de drapeau comme un tout conceptuel ou allez-vous avoir beaucoup de code par drapeau? Quoi qu'il en soit, je pense qu'avoir cela comme classe ou structure de champs de bits à 1 bit pourrait en réalité être plus clair:

struct RecordFlag {
    unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};

Ensuite, votre classe d'enregistrement peut avoir une variable membre struct RecordFlag, les fonctions peuvent prendre des arguments de type struct RecordFlag, etc. Le compilateur doit regrouper les champs de bits pour économiser de l'espace.

Je n'utiliserais probablement pas d'énum pour ce genre de chose où les valeurs peuvent être combinées. Plus généralement, les enum sont des états mutuellement exclusifs.

Mais quelle que soit la méthode que vous utilisez, pour préciser qu'il s'agit de valeurs qui sont des bits pouvant être combinés, utilisez cette syntaxe pour les valeurs réelles:

#define X_NEW      (1 << 0)
#define X_DELETED  (1 << 1)
#define X_MODIFIED (1 << 2)
#define X_EXISTING (1 << 3)

En utilisant un décalage à gauche, cela aide à indiquer que chaque valeur est censée être un bit unique, il est moins probable que plus tard quelqu'un fasse quelque chose de mal, comme ajouter une nouvelle valeur et lui attribuer une valeur de 9.

Basé sur KISS , haute cohésion et faible couplage , posez ces questions -

  • Qui a besoin de savoir? ma classe, ma bibliothèque, d'autres classes, d'autres bibliothèques, des tiers
  • Quel niveau d'abstraction dois-je fournir? Le consommateur comprend-il les opérations sur les bits?
  • Dois-je avoir une interface à partir de VB / C #, etc.?

Il existe un excellent livre & "; Conception de logiciels C ++ à grande échelle " ;, ceci promeut les types de base en externe, si vous pouvez éviter une autre dépendance de fichier d'en-tête / interface, vous devriez essayer.

Si vous utilisez Qt, vous devriez jeter un coup d'œil à QFlags . La classe QFlags fournit un moyen sûr pour le type de stocker des combinaisons OR de valeurs enum.

Je préférerais aller avec

typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;

Tout simplement parce que:

  1. Il est plus propre et rend le code lisible et maintenable.
  2. Il regroupe logiquement les constantes.
  3. Le temps du programmeur est plus important, sauf si votre travail est de sauvegarder ces 3 octets.

Ce n’est pas que j’aime sur-ingénierie de tout, mais dans certains cas, il peut être intéressant de créer une (petite) classe pour encapsuler cette information. Si vous créez une classe RecordType, elle pourrait avoir les fonctions suivantes:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

etc ... (ou ce que la convention convient)

Il pourrait valider des combinaisons (dans le cas où toutes les combinaisons ne sont pas légales, par exemple, si «nouveau» et «supprimé» ne peuvent pas être définis en même temps). Si vous venez d'utiliser des masques de bits, etc., alors le code qui définit l'état doit être validé, une classe peut également encapsuler cette logique.

La classe peut également vous donner la possibilité d'attacher des informations de journalisation significatives à chaque état, vous pouvez ajouter une fonction pour renvoyer une représentation sous forme de chaîne de l'état actuel, etc. (ou utiliser les < & des opérateurs de diffusion en continu. lt; ').

Pour autant, si vous êtes préoccupé par le stockage, vous pouvez toujours demander à la classe de ne disposer que d'un membre de données char, par conséquent, ne prenez qu'une petite quantité de stockage (en supposant qu'il ne soit pas virtuel). Bien sûr, en fonction du matériel, etc., vous pouvez avoir des problèmes d'alignement.

Vous pourriez avoir les valeurs de bits réelles non visibles pour le reste du "monde" si elles se trouvent dans un espace de noms anonyme dans le fichier cpp plutôt que dans le fichier d'en-tête.

Si vous constatez que le code utilisant l'énumération / # define / masque de bits, etc. contient beaucoup de code de 'prise en charge' pour traiter les combinaisons non valides, la journalisation, etc., il peut être intéressant de considérer l'encapsulation dans une classe. Bien sûr, la plupart du temps, les problèmes simples sont résolus par des solutions simples ...

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top