Dans un projet C++ sur lequel je travaille, j'ai une sorte de valeur flag qui peut avoir quatre valeurs. Ces quatre drapeaux peuvent être combinés. Les indicateurs décrivent les enregistrements dans la base de données et peuvent être:
Maintenant, pour chaque enregistrement, je souhaite conserver cet attribut, afin que je puisse utiliser une énumération:
enum { xNew, xDeleted, xModified, xExisting }
Cependant, à d'autres endroits du code, je dois sélectionner les enregistrements qui doivent être visibles par l'utilisateur, donc j'aimerais pouvoir passer cela en tant que paramètre unique, comme:
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;
}
Les exigences d'espace sont importantes (octet vs int) mais pas cruciales. Avec les définitions, je perds la sécurité des types et avec enum
je perds de l'espace (entiers) et je dois probablement transtyper lorsque je veux effectuer une opération au niveau du bit. Avec const
je pense que je perds également la sécurité du type car un _ uint8
pourrait entrer par erreur.
Existe-t-il un autre moyen plus propre?
Sinon, que feriez-vous et pourquoi?
P.S. Le reste du code est un C++ moderne plutôt propre 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 approche unique. Je travaille dans des systèmes embarqués, donc la solution suivante est basée sur le fait que les opérateurs entiers et au niveau du bit sont rapides, peu de mémoire et peu utilisés en 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 type vérifié lors de la compilation. Utilisez toujours la vérification du type de compilation pour vous assurer que les arguments et les variables reçoivent le type correct. Il n'y a pas besoin du typedef 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 retourner 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 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 marqueur d'état vs masque d'état. Cela détectera les bogues car typedef
n'est qu'un int
et une valeur telle que 0xDEADBEEF
peut être dans votre variable via des variables non initialisées ou mal pointé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 des valeurs sont utiles dans les assertions pour intercepter les mauvaises valeurs dès qu'elles sont utilisées. Plus vite vous attrapez un bug lors de l'exécution, moins il peut faire 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 d'assurer une sécurité de valeur correcte est d'utiliser une classe dédiée avec des surcharges d'opérateur et qui est laissée comme exercice pour un autre lecteur.
Ils pollueront votre code.
struct RecordFlag {
unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};
N'utilisez jamais cela . Vous êtes plus soucieux de vitesse que d'économiser 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. Tout d'abord, l'ordre des bits en mémoire varie d'un compilateur à l'autre. En plus, de nombreux compilateurs populaires génèrent du code inefficace pour lire et écrire des membres de bits, et il y a potentiellement de graves problèmes de sécurité des threads concernant les champs de bits (en particulier sur les systèmes multiprocesseurs) du fait que la plupart des machines ne peuvent pas manipuler des ensembles arbitraires de bits en mémoire, mais doivent à la place charger et stocker des mots entiers. Par exemple, ce qui suit ne serait pas thread-safe, 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 des champs de bits, peut-être Raymond Chen vous convaincra dans son The Old New Thing Post: L'analyse 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
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 d'en-tête, leurs valeurs seront intégrées. Vous pourrez utiliser l'interrupteur sur ces valeurs, mais cela augmentera légèrement le couplage.
Ah, oui: supprimez le mot-clé statique . static est déconseillé en C++ lorsqu'il est utilisé comme vous le faites, et si uint8 est un type buildin, 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.
Identique à 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 typedefs 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 énumération pollue l'espace de noms global. Si vous mettez cette énumération 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.
}
}
Si vous souhaitez 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 entrées comme externes dans l'en-tête et comme 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;
}
Cependant, vous ne pourrez pas utiliser le commutateur sur ces constantes. Alors à la fin, choisissez votre poison ... :-p
Avez-vous exclu std :: bitset? Les jeux de drapeaux sont à ça. 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);
Parce qu'il y a un tas de surcharges d'opérateur pour le jeu de bits, vous pouvez maintenant 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 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 définir un seul ensemble de constantes, et les constantes RecordType sont probablement plus utiles.
En supposant que vous avez exclu le jeu de bits, je vote pour le enum.
Je n'achète pas que lancer les énumérations est un grave inconvénient - OK, c'est donc un peu bruyant, et attribuer une valeur hors plage à une énumération est un comportement indéfini, il est donc théoriquement possible de se tirer une balle dans le pied sur un C++ inhabituel implémentations. Mais si vous ne le faites que lorsque cela est nécessaire (c'est-à-dire lorsque vous passez de int à enum iirc), c'est un code parfaitement normal que les gens ont déjà vu.
Je doute aussi de tout coût d'espace de l'énumération. Les variables et paramètres uint8 n'utiliseront probablement pas moins de pile que ints, donc seul le stockage dans les classes compte. Il y a des cas où empaqueter 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 l'avantage de toute façon.
L'énumération n'a donc pas d'inconvénients par rapport aux autres, et comme avantage vous donne un peu de sécurité de type (vous ne pouvez pas attribuer une valeur entière aléatoire sans transtypage explicite) et des moyens propres de se référer à tout.
De préférence, je mettrais également le "= 2" dans l'énumération, soit dit en passant. Ce n'est pas nécessaire, mais un "principe du 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 objets 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 fournissent une "signification aux identificateurs" ainsi qu'une sécurité de type. Vous pouvez clairement dire que "xDeleted" est de "RecordType" et que cela représente le "type d'enregistrement" (wow!) Même après des années. Les consts auraient besoin de commentaires pour cela, ils auraient également besoin de monter et descendre dans le code.
Avec définit je perds la sécurité du 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 (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 à lancer lorsque je veux faire une opération au niveau du bit.
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 que je perds également la sécurité du type car un uint8 aléatoire pourrait entrer par erreur.
La même chose peut se produire avec l'un de ces mécanismes: les vérifications de plage et de valeur sont normalement orthogonales à la sécurité des types (bien que les types définis par l'utilisateur - c'est-à-dire vos propres classes - puissent appliquer des "invariants" à 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'énumération non initialisée, corrompue ou simplement mal définie pourrait toujours finir par interpréter son modèle de bits comme un nombre inattendu - en comparant une inégalité à l'une des les identificateurs d'énumération, toute combinaison d'entre eux, et 0.
Existe-t-il un autre moyen plus propre?/Sinon, que feriez-vous et pourquoi?
Eh bien, au final, le style C éprouvé au niveau du bit OR d'énumérations 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 certains fonctions et assertions de validation personnalisées comme dans la réponse de mat_geek; techniques souvent également applicables à la gestion des chaînes, des valeurs int, des valeurs doubles, etc.
On pourrait 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 se resserrent mais le code augmente considérablement ... dépend du nombre d'objets que vous avez, et les lamdbas - aussi beaux soient-ils - sont toujours plus compliqués et plus difficiles à obtenir que les OR au niveau du bit.
BTW/- l'argument selon lequel la sécurité des threads est assez faible à mon humble avis - se souvient mieux d'être une considération de fond plutôt que de devenir une force de décision dominante; le partage d'un mutex à travers les champs binaires est une pratique plus probable même si vous n'êtes pas au courant de leur empaquetage (les mutex sont des membres de données relativement volumineux - je dois être vraiment préoccupé par les performances pour envisager d'avoir plusieurs mutex sur les membres d'un objet, et je regarderais attentivement assez pour remarquer qu'ils étaient des champs de bits). Tout type de taille inférieure à Word 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 d'échange si vous cherchez désespérément une concurrence plus élevée.
Même si vous devez utiliser 4 octets pour stocker une énumération (je ne suis pas très familier avec C++ - je sais que vous pouvez spécifier le type sous-jacent en C #), cela en vaut la peine - utilisez des énumérations.
À l'heure actuelle des serveurs avec des Go de mémoire, des choses comme 4 octets contre 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 faire en sorte que C++ utilise un octet pour sauvegarder l'énumération), alors vous pouvez considérer la route 'static const'.
À la fin de la journée, vous devez vous demander, est-ce que cela vaut la peine de maintenir l'utilisation de "constante statique" pour les 3 octets d'économie de mémoire pour votre structure de données?
Autre chose à garder à l'esprit - IIRC, sur x86, les structures de données sont alignées sur 4 octets, donc à moins que vous ayez un certain nombre d'éléments de largeur d'octet dans votre structure `` d'enregistrement '', cela pourrait ne pas vraiment avoir d'importance. Testez et assurez-vous que c'est le cas avant de faire un compromis sur la maintenabilité des performances/de l'espace.
Si vous voulez la sécurité des types de classes, avec la commodité de la syntaxe d'énumération et de la vérification des bits, pensez à Safe Labels in C++ . J'ai travaillé avec l'auteur et il est assez intelligent.
Attention cependant. Au final, ce package utilise des modèles et des macros!
Si vous utilisez Qt, vous devriez chercher QFlags . La classe QFlags fournit un moyen sûr pour le stockage des combinaisons OR de valeurs d'énumération.
Avez-vous réellement besoin de faire circuler les valeurs des indicateurs dans leur ensemble conceptuel, ou allez-vous avoir beaucoup de code par indicateur? Quoi qu'il en soit, je pense qu'avoir cela comme classe ou structure de champs de bits 1 bit pourrait en fait être plus clair:
struct RecordFlag {
unsigned isnew:1, isdeleted:1, ismodified:1, isexisting:1;
};
Votre classe d'enregistrement pourrait alors avoir une variable membre struct RecordFlag, les fonctions peuvent prendre des arguments de type struct RecordFlag, etc. Le compilateur devrait regrouper les champs de bits ensemble, économisant de l'espace.
Je n'utiliserais probablement pas une énumération pour ce genre de chose où les valeurs peuvent être combinées ensemble, plus généralement les énumérations sont des états mutuellement exclusifs.
Mais quelle que soit la méthode que vous utilisez, pour qu'il soit plus clair que ce sont des valeurs qui sont des bits qui peuvent être combinés ensemble, 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)
L'utilisation d'un décalage à gauche permet d'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 ajouter une nouvelle valeur et lui attribuer une valeur de 9.
Basé sur BAISER , haute cohésion et faible couplage , posez ces questions -
Il y a un grand livre " Large-Scale C++ Software Design ", cela promeut 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.
Je préfère aller avec
typedef enum { xNew = 1, xDeleted, xModified = 4, xExisting = 8 } RecordType;
Simplement parce que:
Non pas que j'aime tout sur-concevoir, mais parfois dans ces cas, il peut être utile de créer une (petite) classe pour encapsuler ces informations. Si vous créez une classe RecordType, elle peut avoir des fonctions comme:
void setDeleted ();
void clearDeleted ();
bool isDeleted ();
etc ... (ou n'importe quelle convention)
Il pourrait valider les combinaisons (dans le cas où toutes les combinaisons ne sont pas légales, par exemple si "nouveau" et "supprimé" ne pouvaient pas être définis en même temps). Si vous venez d'utiliser des masques de bits, etc., 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 opérateurs de streaming "<<").
Pour autant, si vous êtes préoccupé par le stockage, vous pouvez toujours avoir la classe uniquement un membre de données "char", donc 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 binaires réelles non visibles par le reste du "monde" si elles se trouvent dans un espace de noms anonyme à l'intérieur du fichier cpp plutôt que dans le fichier d'en-tête.
Si vous trouvez que le code utilisant l'énum/# define/bitmask etc a beaucoup de code de "support" pour gérer les combinaisons invalides, la journalisation, etc. l'encapsulation dans une classe peut être utile. Bien sûr, la plupart du temps, les problèmes simples sont mieux avec des solutions simples ...