130 Stimmen

Sollte ich #define, enum oder const verwenden?

In einem C++-Projekt, an dem ich gerade arbeite, habe ich eine Flagge Art von Wert, der vier Werte haben kann. Diese vier Flags können kombiniert werden. Flaggen beschreiben die Datensätze in der Datenbank und können sein:

  • neuer Rekord
  • gelöschter Datensatz
  • geänderter Datensatz
  • bestehende Aufzeichnung

Nun möchte ich für jeden Datensatz dieses Attribut behalten, also könnte ich eine Aufzählung verwenden:

enum { xNew, xDeleted, xModified, xExisting }

An anderen Stellen im Code muss ich jedoch auswählen, welche Datensätze für den Benutzer sichtbar sein sollen, daher würde ich das gerne als einzelnen Parameter übergeben können, z. B:

showRecords(xNew | xDeleted);

Es gibt also drei mögliche Vorgehensweisen:

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

Der Platzbedarf ist wichtig (Byte vs. Int), aber nicht entscheidend. Mit Defines verliere ich die Typsicherheit, und mit enum Ich verliere etwas Platz (ganze Zahlen) und muss wahrscheinlich einen Cast durchführen, wenn ich eine bitweise Operation durchführen möchte. Mit const Ich glaube, ich verliere auch die Typensicherheit, da eine zufällige uint8 versehentlich eindringen könnte.

Gibt es einen anderen, saubereren Weg?

Wenn nicht, was würden Sie verwenden und warum?

P.S. Der Rest des Codes ist ziemlich sauberes modernes C++ ohne #define s, und ich habe Namespaces und Templates in wenigen Räumen verwendet, so dass diese auch nicht außer Frage stehen.

91voto

mat_geek Punkte 2450

Kombinieren Sie die Strategien, um die Nachteile eines einzelnen Ansatzes zu verringern. Da ich mit eingebetteten Systemen arbeite, basiert die folgende Lösung auf der Tatsache, dass Ganzzahl- und bitweise Operatoren schnell sind und wenig Speicher und Flash benötigen.

Legen Sie die Aufzählung in einen Namespace, um zu verhindern, dass die Konstanten den globalen Namespace verschmutzen.

namespace RecordType {

Ein Enum deklariert und definiert eine zur Kompilierzeit überprüfte Typisierung. Verwenden Sie immer die Typüberprüfung zur Kompilierzeit, um sicherzustellen, dass Argumente und Variablen den richtigen Typ haben. In C++ besteht keine Notwendigkeit für ein typedef.

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

Erstellen Sie ein weiteres Mitglied für einen ungültigen Zustand. Dies kann als Fehlercode nützlich sein, z. B. wenn Sie den Zustand zurückgeben wollen, aber die E/A-Operation fehlschlägt. Es ist auch für die Fehlersuche nützlich; verwenden Sie es in Initialisierungslisten und Destruktoren, um zu wissen, ob der Wert der Variablen verwendet werden soll.

xInvalid = 16 };

Bedenken Sie, dass Sie mit diesem Typ zwei Ziele verfolgen. Den aktuellen Zustand eines Datensatzes zu verfolgen und eine Maske zu erstellen, um Datensätze in bestimmten Zuständen auszuwählen. Erstellen Sie eine Inline-Funktion, um zu testen, ob der Wert des Typs für Ihren Zweck gültig ist; als Zustandsmarker oder als Zustandsmaske. Dies fängt Fehler ab, da die typedef ist nur eine int und einen Wert wie 0xDEADBEEF kann durch nicht initialisierte oder falsch platzierte Variablen in Ihrer Variable enthalten sein.

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

Hinzufügen einer using Richtlinie, wenn Sie den Typ häufig verwenden wollen.

using RecordType ::TRecordType ;

Die Wertüberprüfungsfunktionen sind in Asserts nützlich, um schlechte Werte abzufangen, sobald sie verwendet werden. Je schneller Sie einen Fehler bei der Ausführung erkennen, desto weniger Schaden kann er anrichten.

Hier sind einige Beispiele, um das Ganze zusammenzufassen.

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

Die einzige Möglichkeit, eine korrekte Wertsicherheit zu gewährleisten, ist die Verwendung einer speziellen Klasse mit Operatorüberladungen, und das soll als Übung für einen anderen Leser dienen.

58voto

paercebal Punkte 78198

Vergessen Sie die Definitionen

Sie werden Ihren Code verschmutzen.

bitfields?

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

Benutzen Sie das niemals . Es geht Ihnen mehr um die Geschwindigkeit als um die Einsparung von 4 Ints. Die Verwendung von Bit-Feldern ist tatsächlich langsamer als der Zugriff auf jeden anderen Typ.

Bit-Members in Structs haben jedoch praktische Nachteile. Erstens variiert die Anordnung der Bits im Speicher von Compiler zu Compiler. Darüber hinaus, viele gängige Compiler erzeugen ineffizienten Code für das Lesen und Schreiben von Bitelementen und es gibt potenziell schwerwiegende Fragen der Gewindesicherheit in Bezug auf Bit-Felder (insbesondere auf Multiprozessorsystemen), da die meisten Maschinen nicht beliebige Bit-Sätze im Speicher manipulieren können, sondern stattdessen ganze Wörter laden und speichern müssen. z.B. wäre das Folgende trotz der Verwendung eines Mutex nicht thread-sicher

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

Und wenn Sie noch mehr Gründe brauchen, um no Bitfelder verwenden, vielleicht Raymond Chen wird Sie in seinem Die alte neue Sache Beitrag: Die Kosten-Nutzen-Analyse von Bitfeldern für eine Sammlung von Booleschen Werten unter 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;
}

Sie in einen Namensraum zu packen ist cool. Wenn sie in Ihrer CPP- oder Header-Datei deklariert sind, werden ihre Werte inlined. Sie werden in der Lage sein, switch auf diese Werte zu verwenden, aber es wird die Kopplung leicht erhöhen.

Ah, ja: das Schlüsselwort static entfernen . static ist in C++ veraltet, wenn es so verwendet wird, wie Sie es tun, und wenn uint8 ein Buildin-Typ ist, brauchen Sie dies nicht in einem Header zu deklarieren, der von mehreren Quellen desselben Moduls eingebunden wird. Am Ende sollte der Code so aussehen:

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

Das Problem bei diesem Ansatz ist, dass Ihr Code den Wert Ihrer Konstanten kennt, was die Kopplung leicht erhöht.

enum

Dasselbe wie const int, mit einer etwas stärkeren Typisierung.

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

Sie verschmutzen aber immer noch den globalen Namensraum. Übrigens... Entfernen Sie den Typedef . Sie arbeiten in C++. Diese Typedefs von Enums und Structs verschmutzen den Code mehr als alles andere.

Das Ergebnis ist irgendwie:

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

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

Wie Sie sehen, verschmutzt Ihre Enum den globalen Namespace. Wenn Sie diese Aufzählung in einen Namespace stellen, haben Sie etwas wie:

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 ?

Wenn Sie die Kopplung verringern wollen (d.h. die Werte der Konstanten ausblenden und so nach Belieben ändern können, ohne dass eine vollständige Neukompilierung erforderlich ist), können Sie die ints als extern im Header und als Konstante in der CPP-Datei deklarieren, wie im folgenden Beispiel:

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

Und:

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

Für diese Konstanten können Sie jedoch nicht den Schalter verwenden. Wählen Sie also am Ende Ihr Gift... :-p

30voto

Steve Jessop Punkte 264569

Haben Sie std::bitset ausgeschlossen? Sets von Flags sind genau das, wofür es da ist. Machen Sie

typedef std::bitset<4> RecordType;

dann

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

Da es eine Reihe von Operatorüberladungen für Bitset gibt, können Sie nun Folgendes tun

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

Oder etwas sehr Ähnliches - ich wäre für jede Korrektur dankbar, da ich dies nicht getestet habe. Sie können auch auf die Bits durch Index verweisen, aber es ist im Allgemeinen am besten, nur einen Satz von Konstanten zu definieren, und RecordType-Konstanten sind wahrscheinlich nützlicher.

Vorausgesetzt, Sie haben Bitset ausgeschlossen, stimme ich für die enum .

Ich glaube nicht, dass das Casting der Enums ein ernsthafter Nachteil ist - OK, es ist ein bisschen laut, und die Zuweisung eines Wertes außerhalb des Bereichs zu einem Enum ist undefiniertes Verhalten, so dass es theoretisch möglich ist, sich bei einigen ungewöhnlichen C++-Implementierungen in den Fuß zu schießen. Aber wenn Sie es nur tun, wenn es notwendig ist (was ist, wenn von int zu enum iirc gehen), es ist ganz normaler Code, die Menschen vor gesehen haben.

Ich bezweifle auch, dass die enum Platz kostet. uint8-Variablen und -Parameter benötigen wahrscheinlich nicht weniger Stack als ints, so dass nur die Speicherung in Klassen von Bedeutung ist. Es gibt einige Fälle, in denen das Packen von mehreren Bytes in eine Struktur Vorteile bringt (in diesem Fall können Sie Enums in und aus dem uint8-Speicher casten), aber normalerweise macht das Auffüllen den Vorteil sowieso zunichte.

Die Enum hat also keine Nachteile im Vergleich zu den anderen, und als Vorteil bietet sie ein wenig Typsicherheit (man kann nicht irgendeinen zufälligen Integer-Wert zuweisen, ohne explizit zu casten) und saubere Möglichkeiten, auf alles zu verweisen.

Ich würde übrigens auch das "= 2" in die Aufzählung aufnehmen. Es ist nicht notwendig, aber das "Prinzip des geringsten Erstaunens" legt nahe, dass alle 4 Definitionen gleich aussehen sollten.

8voto

Abbas Punkte 6570

Hier sind ein paar Artikel über const vs. Makros vs. enums:

Symbolische Konstanten
Aufzählung Konstanten vs. Konstante Objekte

Ich denke, Sie sollten Makros vermeiden, zumal Sie den Großteil Ihres neuen Codes in modernem C++ geschrieben haben.

5voto

Tony Delroy Punkte 98528

Bei Definitionen verliere ich die Typensicherheit

Nicht unbedingt...

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

und mit enum verliere ich etwas Platz (ganze Zahlen)

Nicht notwendigerweise - aber man muss bei den Speicherpunkten explizit sein...

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

und muss wahrscheinlich casten, wenn ich eine bitweise Operation durchführen möchte.

Sie können Operatoren erstellen, die Ihnen diese Arbeit abnehmen:

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

Mit const denke ich, dass ich auch die Typsicherheit verliere, da eine zufällige uint8 aus Versehen hineingehen könnte.

Dasselbe kann mit jedem dieser Mechanismen passieren: Bereichs- und Wertprüfungen sind normalerweise orthogonal zur Typsicherheit (obwohl benutzerdefinierte Typen - d.h. Ihre eigenen Klassen - "Invarianten" über ihre Daten erzwingen können). Bei Enums steht es dem Compiler frei, einen größeren Typ für die Werte zu wählen, und eine nicht initialisierte, beschädigte oder einfach nur falsch gesetzte Enum-Variable könnte ihr Bitmuster immer noch als eine Zahl interpretieren, die man nicht erwarten würde - ein Vergleich ungleich einem der Enum-Bezeichner, einer beliebigen Kombination davon oder 0.

Gibt es einen anderen, saubereren Weg? / Wenn nicht, was würden Sie verwenden und warum?

Nun, letztendlich funktioniert das bewährte bitweise ODER von Aufzählungen im C-Stil ziemlich gut, sobald man Bitfelder und benutzerdefinierte Operatoren im Spiel hat. Sie können Ihre Robustheit mit einigen benutzerdefinierten Validierungsfunktionen und Assertions wie in der Antwort von mat_geek weiter verbessern; Techniken, die oft auch für die Verarbeitung von String-, Int-, Double-Werten usw. anwendbar sind.

Man könnte argumentieren, dass dies "sauberer" ist:

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

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

Ich bin gleichgültig: die Datenbits packen enger, aber der Code wächst erheblich ... hängt davon ab, wie viele Objekte Sie haben, und die Lamdbas - schön wie sie sind - sind immer noch chaotischer und schwieriger, richtig als bitweise ODERs zu bekommen.

BTW /- das Argument über die Threadsicherheit ist ziemlich schwach IMHO - am besten als Hintergrundüberlegung erinnert, anstatt eine dominante Entscheidung treibende Kraft zu werden; teilen eine Mutex über die Bitfelder ist eine wahrscheinliche Praxis, auch wenn nicht bewusst ihre Verpackung (Mutexe sind relativ sperrige Daten Mitglieder - ich muss wirklich besorgt über die Leistung in Betracht ziehen, mehrere Mutexe auf Mitglieder eines Objekts zu haben, und ich würde genau genug aussehen, um zu bemerken, dass sie Bitfelder waren). Jeder Typ, der eine geringere Wortgröße hat, könnte das gleiche Problem haben (z.B. ein uint8_t ). Wie auch immer, Sie könnten atomare Operationen im Stil von Compare-and-Swap versuchen, wenn Sie verzweifelt nach höherer Gleichzeitigkeit suchen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X