Devrais-je utiliser #define, enum ou const?

Dans un projet C ++ sur lequel je travaille, j’ai un type de valeur de drapeau qui peut avoir quatre valeurs. Ces quatre drapeaux peuvent être combinés. Les drapeaux décrivent les enregistrements dans la firebase database et peuvent être:

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

Maintenant, pour chaque enregistrement, je souhaite conserver cet atsortingbut, afin que je puisse utiliser un enum:

enum { xNew, xDeleted, xModified, xExisting } 

Cependant, dans d’autres endroits du code, je dois sélectionner les enregistrements qui doivent être visibles par l’utilisateur, je voudrais donc pouvoir les transmettre en un seul paramètre, comme:

 showRecords(xNew | xDeleted); 

Donc, il me semble avoir trois possibilités:

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

Les exigences d’espace sont importantes (octet vs int) mais pas cruciales. Avec les définitions, je perds le type de sécurité, et avec enum je perds de l’espace (entiers) et je dois probablement lancer quand je veux effectuer une opération au niveau du bit. Avec const je pense aussi que je perds la sécurité de type car un uint8 aléatoire pourrait entrer par erreur.

Y a-t-il un autre moyen plus propre?

Si non, que feriez-vous et pourquoi?

PS Le rest du code est plutôt C ++ moderne sans #define s, et j’ai utilisé des espaces de noms et des modèles dans quelques espaces, donc ils ne sont pas non plus hors de question.

Combinez les stratégies pour réduire les inconvénients d’une seule approche. Je travaille dans des systèmes embarqués. La solution suivante est basée sur le fait que les opérateurs entiers et binarys sont rapides, à faible mémoire et à faible utilisation de mémoire 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 énumération déclare et définit un temps de compilation vérifié. Utilisez toujours la vérification du type de compilation pour vous assurer que les arguments et les variables ont le bon type. Le typedef n’est pas nécessaire en C ++.

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

Créez un autre membre pour un état non valide. Cela peut être utile comme code d’erreur; Par exemple, lorsque vous souhaitez renvoyer l’état mais que l’opération d’E / S échoue. Il 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 objectives 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 objective. comme marqueur d’état vs masque d’état. Cela va attraper des bogues car le typedef est juste un int et une valeur telle que 0xDEADBEEF peut se trouver dans votre variable par le biais de variables non initialisées ou mal placé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 le type souvent.

 using RecordType ::TRecordType ; 

Les fonctions de vérification de la valeur sont utiles dans les assertions pour intercepter les mauvaises valeurs dès qu'elles sont utilisées. Plus vous attrapez un bug rapidement lors de la course, moins il y a de dégâts.

Voici quelques exemples pour tout rassembler.

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

La seule façon de garantir une sécurité correcte des valeurs consiste à utiliser une classe dédiée avec des surcharges d'opérateur et qui rest un exercice pour un autre lecteur.

Oubliez les définitions

Ils vont polluer votre code.

champs de bits?

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

Ne jamais utiliser ça . 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’access à tout autre type.

Cependant, les membres de bit dans les structures ont des inconvénients pratiques. Tout d’abord, le classement des bits en mémoire varie d’un compilateur à l’autre. En outre, de nombreux compilateurs populaires génèrent un code inefficace pour la lecture et l’écriture des 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), car la plupart des machines ne peuvent mais doit plutôt charger et stocker des mots entiers. par exemple, les éléments suivants ne seraient pas compatibles avec les threads, malgré l’utilisation d’un mutex

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

Et si vous avez besoin de plus de raisons pour ne pas utiliser les champs de bits, peut-être que Raymond Chen vous convaincra dans The Old New Thing Post: L’parsing coûts-avantages des champs de bits pour une collection de booléens sur 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 en-tête, leurs valeurs seront intégrées. Vous pourrez utiliser ces valeurs, mais cela augmentera légèrement le couplage.

Ah, oui: supprime le mot-clé statique . static est obsolète en C ++ lorsqu’il est utilisé comme vous, et si uint8 est un type buildin, vous n’en aurez pas besoin pour déclarer cela 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.

enum

La même chose que const int, avec un typage un peu plus fort.

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

Cependant, ils polluent toujours l’espace de noms mondial. Au fait … Supprimez le typedef . Vous travaillez en C ++. Ces types de é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 enum dans un espace de noms, vous aurez 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 voulez diminuer le couplage (c’est-à-dire pouvoir masquer les valeurs des constantes, et donc les modifier comme vous le souhaitez sans avoir besoin d’une recompilation complète), vous pouvez déclarer les ints comme externes dans l’en-tête et constantes 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; } 

Vous ne pourrez cependant pas utiliser ces constantes. Alors à la fin, choisissez votre poison … :-p

Avez-vous exclu std :: bitset? Les ensembles de drapeaux sont ce à quoi ça sert. 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 un tas de surcharges d’opérateurs pour les bitset, 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 – j’apprécierais toute correction puisque 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 définir un seul ensemble de constantes, et les constantes RecordType sont probablement plus utiles.

En supposant que vous avez exclu les bitset, je vote pour l’ énumération .

Je n’achète pas le fait de jeter les énumérations est un désavantage sérieux – OK, donc c’est un peu bruyant, et l’atsortingbution d’une valeur hors de scope à un enum est un comportement indéfini, il est donc théoriquement possible de se tirer dans le pied implémentations. Mais si vous ne le faites que lorsque cela est nécessaire (ce qui est le cas lorsque vous passez de l’int à l’énumération iirc), c’est un code parfaitement normal que les gens ont déjà vu.

Je doute de la question du coût en espace de l’enum aussi. Les variables et parameters uint8 n’utiliseront probablement pas moins de stack que ints, par conséquent, seul le stockage dans les classes est important. Il y a des cas où le regroupement de plusieurs octets dans une structure gagnera (dans ce cas, vous pouvez lancer des énumérations dans et hors du stockage uint8), mais normalement, le remplissage supprimera tout le bénéfice.

Ainsi, l’énumération ne présente aucun inconvénient par rapport aux autres, et comme avantage, vous bénéficiez d’un peu de sécurité de type (vous ne pouvez pas atsortingbuer une valeur entière aléatoire sans conversion explicite) et de méthodes de référence claires à tout.

De préférence, je mettrais aussi le “= 2” dans la liste, au fait. Ce n’est pas nécessaire, mais un “principe de moindre étonnement” suggère que les 4 définitions devraient se ressembler.

Voici quelques articles sur const vs macros vs enums:

Constantes symboliques
Constantes d’énumération vs objects constants

Je pense que vous devriez éviter les macros, d’autant plus que vous avez écrit la plupart 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 fourniraient “un sens aux identifiants” ainsi qu’un type de sécurité. Vous pouvez clairement dire que “xDeleted” est de “RecordType” et qu’il représente “type de disque” (wow!) Même après des années. Les consts nécessiteraient des commentaires pour cela, ils nécessiteraient aussi de monter et descendre dans le code.

Avec des définitions, je perds le type de sécurité

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 (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 devoir lancer quand je veux faire l’opération de bitwise.

Vous pouvez créer des opérateurs pour éliminer la douleur:

 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 se produire avec n’importe lequel de ces mécanismes: les vérifications de distance et de valeur sont normalement orthogonales à la sécurité de type (bien que les types définis par l’utilisateur – vos propres classes – puissent imposer des invariants ». Avec les énumérations, le compilateur est libre de choisir un type plus grand pour héberger les valeurs, et une variable enum non initialisée, corrompue ou simplement erronée pourrait toujours interpréter son modèle de bits comme un nombre inattendu – en comparant inégalement les identifiants d’énumération, toute combinaison de ceux-ci, et 0.

Y a-t-il un autre moyen plus propre? / Sinon, que feriez-vous et pourquoi?

Eh bien, à la fin, le OU de bit du style C éprouvé et approuvé fonctionne assez 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 des fonctions de validation et des assertions personnalisées, comme dans la réponse de mat_geek; techniques souvent également applicables à la manipulation de chaînes, int, valeurs doubles, etc.

Vous pourriez soutenir 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 compacts mais le code augmente de manière significative … cela dépend du nombre d’objects que vous possédez, et les lamdbas, aussi beaux soient-ils, sont encore plus difficiles à obtenir que les blocs de bits.

BTW / – l’argument de la sécurité des threads est plutôt faible à mon humble avis – mieux mémorisé comme une considération de fond plutôt que de devenir une force mosortingce dominante; le partage d’un mutex à travers les champs de bits est une pratique plus probable même s’il n’est pas conscient de leur empaquetage (les mutex sont des membres de données relativement volumineux – je dois vraiment m’inquiéter des performances pour envisager d’avoir plusieurs mutex sur les membres d’un object) 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 de comparaison et d’échange atomiques si vous êtes à la recherche d’une concurrence accrue.

Même si vous devez utiliser 4 octets pour stocker un enum (je ne suis pas familier avec C ++ – je sais que vous pouvez spécifier le type sous-jacent en C #), cela en vaut toujours la peine – utilisez des énumérations.

De nos jours, avec des serveurs avec des Go de mémoire, des choses comme 4 octets par rapport à 1 octet de mémoire au niveau de l’application en général n’ont pas d’importance. Bien sûr, si dans votre situation particulière l’utilisation de la mémoire est si importante (et que vous ne pouvez pas utiliser C ++ pour utiliser un octet pour sauvegarder l’énumération), vous pouvez envisager la route “statique”.

À la fin de la journée, vous devez vous demander si l’utilisation de «statique const» pour les 3 octets d’économies de mémoire de votre structure de données vaut le coup.

Autre chose à garder à l’esprit – IIRC, sur x86, les structures de données sont alignées sur 4 octets, donc à moins d’avoir un certain nombre d’éléments de largeur d’octet dans votre structure «d’enregistrement», cela n’a peut-être pas vraiment d’importance. Testez et assurez-vous de le faire avant de faire un compromis en matière de maintenabilité des performances / de l’espace.

Si vous voulez le type de sécurité des classes, avec la commodité de la syntaxe d’énumération et de la vérification des bits, considérez les étiquettes sécurisées en C ++ . J’ai travaillé avec l’auteur, et il est très intelligent.

Attention cependant. Au final, ce paquet utilise des templates et des macros!

Avez-vous réellement besoin de faire circuler les valeurs des drapeaux dans un ensemble conceptuel, ou allez-vous avoir beaucoup de code par drapeau? Quoi qu’il en soit, je pense qu’il est plus clair d’avoir cette classe ou structure de bitfields à 1 bit:

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

Ensuite, votre classe d’enregistrement pourrait avoir une variable membre struct RecordFlag, les fonctions pouvant prendre des arguments de type struct RecordFlag, etc.

Je ne voudrais probablement pas utiliser un enum pour ce genre de chose où les valeurs peuvent être combinées, plus typiquement les énumérations sont des états mutuellement exclusifs.

Mais quelle que soit la méthode que vous utilisez, pour rendre plus clair que ce sont des valeurs qui peuvent être combinées, utilisez plutôt 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) 

Utiliser un décalage vers la gauche aide à indiquer que chaque valeur est destinée à être un seul bit, il est moins probable que plus tard quelqu'un fasse quelque chose de mal comme append une nouvelle valeur et lui atsortingbuer une valeur de 9.

Sur la base de 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?
  • Est-ce que je devrai interfacer de VB / C # etc.?

Il y a un excellent livre ” Conception de logiciel C ++ à grande échelle “, qui favorise les types de base en externe, si vous pouvez éviter une autre dépendance de fichier d’en-tête / interface que vous devriez essayer.

Si vous utilisez Qt, vous devriez chercher des QFlags . La classe QFlags fournit un moyen sûr de stocker des combinaisons OR de valeurs enum.

Je préfère aller avec

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

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, à moins que votre travail ne consiste à enregistrer ces 3 octets.

Ce n’est pas que j’aime trop tout faire, mais dans ces cas, il peut être utile de créer une (petite) classe pour encapsuler ces informations. Si vous créez une classe RecordType, il pourrait avoir des fonctions telles que:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

etc … (ou quelque convention que ce soit)

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 tous deux définis en même temps). Si vous avez simplement utilisé des masques de bits, alors le code qui définit l’état doit être validé, une classe peut également encapsuler cette logique.

La classe peut également vous permettre d’attacher des informations de journalisation significatives à chaque état, vous pouvez append une fonction pour renvoyer une représentation sous forme de chaîne de l’état actuel, etc. (ou utiliser les opérateurs de diffusion en continu ‘< <).

Pour tout cela, si vous êtes préoccupé par le stockage, vous pourriez toujours avoir une classe contenant uniquement un membre de données ‘char’, donc ne prenez qu’une petite quantité de stockage (en supposant qu’elle ne soit pas virtuelle). Bien sûr, en fonction du matériel, etc., vous pouvez avoir des problèmes d’alignement.

Vous pourriez avoir les valeurs de bit réelles non visibles pour le rest 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 trouvez que le code utilisant le enum / # define / bitmask etc. a beaucoup de code ‘support’ pour traiter les combinaisons non valides, la journalisation etc. alors l’encapsulation dans une classe peut être utile. Bien sûr, la plupart du temps, les problèmes simples sont plus faciles avec des solutions simples …