123 votes

Dois-je utiliser #define, enum ou const ?

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

  • nouveau record
  • enregistrement supprimé
  • dossier modifié
  • dossier existant

Maintenant, pour chaque enregistrement, je souhaite conserver cet attribut, je pourrais donc utiliser une énumération :

enum { xNew, xDeleted, xModified, xExisting }

Cependant, à d'autres endroits dans le code, j'ai besoin de sélectionner les enregistrements qui doivent être visibles pour l'utilisateur, et j'aimerais donc pouvoir passer cela comme un paramètre unique, comme :

showRecords(xNew | xDeleted);

Donc, il semble 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 pas crucial. Avec les définitions, je perds la sécurité des types, et avec enum Je perds un peu d'espace (nombres entiers) et je dois probablement effectuer un cast lorsque je veux faire une opération bit à bit. Avec const Je pense que je perds aussi la sécurité de type puisqu'un aléa uint8 pourrait entrer par erreur.

Existe-t-il un autre moyen plus propre ?

Si non, qu'utiliseriez-vous et pourquoi ?

P.S. Le reste du code est du C++ moderne plutôt propre, sans #define et j'ai utilisé des espaces de noms et des modèles dans quelques espaces, donc ceux-ci ne sont pas hors de question non plus.

87voto

mat_geek Points 1367

Combiner les stratégies pour réduire les inconvénients d'une seule approche. Je travaille dans le domaine des systèmes embarqués. La solution suivante est donc basée sur le fait que les opérateurs sur les nombres entiers et sur les bits sont rapides et utilisent peu de mémoire et de flash.

Placez l'enum dans un espace de nom pour éviter que les constantes ne polluent l'espace de nom global.

namespace RecordType {

Un enum déclare et définit un typage vérifié au moment de la compilation. Utilisez toujours la vérification du type au moment de la 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éer un autre membre pour un état invalide. Cela peut être utile comme code d'erreur ; par exemple, lorsque vous voulez 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. Suivre l'état actuel d'un enregistrement et créer un masque pour sélectionner les 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 ou comme masque d'état. Cela permettra d'éviter les bogues, car la fonction 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 positionné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 ;
}

Ajouter un using si vous voulez utiliser le type souvent.

using RecordType ::TRecordType ;

Les fonctions de vérification des valeurs sont utiles dans les assertions pour piéger les mauvaises valeurs dès qu'elles sont utilisées. Plus vite vous attrapez un bogue en cours d'exécution, moins il peut faire de dégâts.

Voici quelques exemples pour illustrer le tout.

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é correcte des valeurs est d'utiliser une classe dédiée avec des surcharges d'opérateurs, ce qui constitue un exercice pour un autre lecteur.

54voto

paercebal Points 38526

Oubliez les définitions

Ils vont polluer votre code.

bitfields ?

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

N'utilise jamais ça. . Vous êtes plus préoccupé par la vitesse que par l'économie de 4 ints. L'utilisation de champs de bits est en fait plus lente que l'accès à tout autre type.

Cependant, les membres binaires dans les structs présentent des inconvénients pratiques. Tout d'abord, l'ordre des bits en mémoire varie d'un compilateur à l'autre. De plus, de nombreux compilateurs populaires génèrent un code inefficace pour la lecture et l'écriture des membres de bit et il existe des risques potentiellement graves les problèmes de sécurité des fils concernant les champs de bits (en particulier sur les systèmes multiprocesseurs) en raison du fait que la plupart des machines ne peuvent pas manipuler des ensembles arbitraires de bits en mémoire, mais doivent au contraire charger et stocker des mots entiers. Par exemple, l'exemple suivant 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 pas utiliser des champs de bits, peut-être Raymond Chen vous convaincra dans son L'ancien et le nouveau truc Poste : L'analyse coût-bénéfice 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 mettre dans un espace de nom est cool. S'ils sont déclarés dans votre CPP ou votre fichier d'en-tête, leurs valeurs seront inlined. Vous pourrez utiliser switch sur ces valeurs, mais cela augmentera légèrement le couplage.

Ah, oui : supprimer le mot-clé statique . static est déprécié en C++ lorsqu'il est utilisé comme vous le faites, et si uint8 est un type buildin, vous n'aurez pas besoin de 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.

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;

Ils continuent cependant à polluer l'espace de noms global. D'ailleurs... Supprimez le typedef . Vous travaillez en C++. Ces typedefs d'enums et de structs polluent le code plus qu'autre chose.

Le résultat est sympathique :

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 nom global. Si vous mettez cet enum dans un espace de nom, 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 réduire le couplage (c'est-à-dire être capable de cacher les valeurs des constantes, et donc de les modifier à volonté sans avoir besoin d'une recompilation complète), vous pouvez déclarer les ints comme extern 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;
}

Vous ne pourrez pas utiliser switch sur ces constantes, cependant. Donc, en fin de compte, choisissez votre poison... :-p

30voto

Steve Jessop Points 166970

Avez-vous exclu std::bitset ? Les jeux de drapeaux sont faits pour ça. Faites

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érateurs pour bitset, 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 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 ne définir qu'un seul ensemble de constantes, et les constantes RecordType sont probablement plus utiles.

En supposant que vous avez écarté le bitset, je vote pour le enum .

Je ne crois pas que le casting des enums soit un désavantage sérieux - OK, c'est un peu bruyant, et l'assignation d'une valeur hors gamme à un enum est un comportement indéfini, donc il est théoriquement possible de se tirer une balle dans le pied sur certaines implémentations C++ inhabituelles. Mais si vous ne le faites que lorsque c'est nécessaire (ce qui est le cas lorsque vous passez de int à enum), c'est un code parfaitement normal que les gens ont déjà vu.

Les variables et paramètres uint8 n'utiliseront probablement pas moins de pile que les ints, donc seul le stockage dans les classes compte. Il y a des cas où l'empaquetage de plusieurs octets dans un struct sera gagnant (dans ce cas, vous pouvez faire entrer et sortir les enums du stockage uint8), mais normalement, le remplissage tuera le bénéfice de toute façon.

L'enum n'a donc pas d'inconvénient par rapport aux autres, et a l'avantage de vous donner un peu de sécurité de type (vous ne pouvez pas attribuer une valeur entière aléatoire sans faire un casting explicite) et des moyens propres de faire référence à tout.

Par préférence, je mettrais aussi le "= 2" dans l'énumération, d'ailleurs. Ce n'est pas nécessaire, mais le "principe du moindre étonnement" suggère que les 4 définitions devraient se ressembler.

8voto

Abbas Points 3737

Voici quelques articles sur les const, les macros et les enums :

Constantes symboliques
Constantes d'énumération et objets constants

Je pense que vous devriez éviter les macros, d'autant plus que la plupart de votre nouveau code est en C++ moderne.

5voto

INS Points 5679

Si possible, n'utilisez PAS de macros. Elles ne sont pas très appréciées dans le C++ moderne.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X