Soll ich #define, enum oder const verwenden?

125

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

  • Neuer Eintrag
  • Datensatz gelöscht
  • geänderter Datensatz
  • vorhandener Datensatz

Nun möchte ich für jeden Datensatz dieses Attribut beibehalten, damit ich eine Aufzählung verwenden kann:

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, damit ich dies als einzelnen Parameter übergeben kann, z.

showRecords(xNew | xDeleted);

Es scheint also, dass ich drei mögliche Ansätze habe:

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

oder

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

oder

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 Definitionen verliere ich die Typensicherheit und mit enumetwas Speicherplatz (Ganzzahlen) und muss wahrscheinlich umwandeln, wenn ich eine bitweise Operation ausführen möchte. Mit constI ich auch verlieren Art Sicherheit denken , da eine zufällige uint8könnte irrtümlich erhalten.

Gibt es einen anderen saubereren Weg?

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

PS Der Rest des Codes ist ziemlich sauber, modernes C ++ ohne #defines, und ich habe Namespaces und Vorlagen in wenigen Räumen verwendet, daher kommen diese auch nicht in Frage.

Milan Babuškov
quelle
"Mit enum verliere ich etwas Platz (ganze Zahlen)". Nicht unbedingt. Siehe stackoverflow.com/questions/366017/… und stackoverflow.com/questions/1113855/… (und gccs -fshort-enum . (Ich gehe davon aus, dass diese C-Antworten in C ++ immer noch zutreffen.)
idbrii
@pydave Falls Sie sich über die Kompatibilität von C und C ++ nicht sicher sind, finde ich diesen Link sehr hilfreich, siehe zum Beispiel enum david.tribble.com/text/cdiffs.htm#C99-enum-type
aka.nice
3
Dies ist ein älteres Thema mit hohen Stimmen. Gibt es einen Grund, C ++ 11-Aufzählungsklassen für diese Problemsituation nicht zu erwähnen?
Brandin
Kombiniert als Hinweis enum RecordType : uint8_tdie Typensicherheit von enummit der geringen Größe von uint8_t, obwohl Sie immer noch bitweise Operatoren bereitstellen müssen.
Justin Time - Stellen Sie Monica

Antworten:

88

Kombinieren Sie die Strategien, um die Nachteile eines einzelnen Ansatzes zu verringern. Ich arbeite in eingebetteten Systemen, daher basiert die folgende Lösung auf der Tatsache, dass ganzzahlige und bitweise Operatoren schnell, wenig Speicher und wenig Flash-Nutzung sind.

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

namespace RecordType {

Eine Aufzählung deklariert und definiert eine aktivierte Kompilierungszeit. Verwenden Sie immer die Typprüfung zur Kompilierungszeit, um sicherzustellen, dass Argumente und Variablen den richtigen Typ erhalten. Typedef ist in C ++ nicht erforderlich.

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

Erstellen Sie ein anderes Mitglied für einen ungültigen Status. Dies kann als Fehlercode nützlich sein. Zum Beispiel, wenn Sie den Status zurückgeben möchten, der E / A-Vorgang jedoch fehlschlägt. Es ist auch nützlich zum Debuggen; Verwenden Sie es in Initialisierungslisten und Destruktoren, um festzustellen, ob der Wert der Variablen verwendet werden soll.

xInvalid = 16 };

Beachten Sie, dass Sie für diesen Typ zwei Zwecke haben. Verfolgen des aktuellen Status eines Datensatzes und Erstellen einer Maske zum Auswählen von Datensätzen in bestimmten Status. Erstellen Sie eine Inline-Funktion, um zu testen, ob der Wert des Typs für Ihren Zweck gültig ist. als Zustandsmarker gegen eine Zustandsmaske. Dies wird Fehler auffangen, da dies typedefnur ein intund ein Wert ist, wie er 0xDEADBEEFin Ihrer Variablen durch nicht initialisierte oder falsch ausgerichtete Variablen enthalten sein kann.

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

Fügen Sie eine usingDirektive hinzu, wenn Sie den Typ häufig verwenden möchten.

using RecordType ::TRecordType ;

Die Werteprüfungsfunktionen sind nützlich bei Asserts, um fehlerhafte Werte abzufangen, sobald sie verwendet werden. Je schneller Sie beim Laufen einen Fehler bemerken, desto weniger Schaden kann er anrichten.

Hier sind einige Beispiele, um alles zusammenzustellen.

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, die Sicherheit korrekter Werte zu gewährleisten, besteht darin, eine spezielle Klasse mit Bedienerüberlastungen zu verwenden, die als Übung für einen anderen Leser verbleibt.

mat_geek
quelle
1
Meistens eine nette Antwort - aber die Frage besagt, dass die Flags kombiniert werden können und die Funktion IsValidState () nicht zulässt, dass sie kombiniert werden.
Jonathan Leffler
3
@ Jonathan Leffler: Von meinem Standpunkt aus denke ich, dass der 'IsValidState' das nicht tun soll, 'IsValidMask' ist.
João Portela
1
Ist es erwünscht, dass IsValidMaskkeine (dh 0) ausgewählt werden kann?
Joachim Sauer
2
−1 Die Idee der Laufzeit-Typprüfung ist ein Gräuel.
Prost und hth. - Alf
54

Vergiss die Definitionen

Sie werden Ihren Code verschmutzen.

Bitfelder?

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

Benutze das niemals . Es geht Ihnen mehr um Geschwindigkeit als um die Einsparung von 4 Zoll. Die Verwendung von Bitfeldern ist tatsächlich langsamer als der Zugriff auf einen anderen Typ.

Bitelemente in Strukturen haben jedoch praktische Nachteile. Erstens variiert die Reihenfolge der Bits im Speicher von Compiler zu Compiler. Darüber hinaus generieren viele gängige Compiler ineffizienten Code zum Lesen und Schreiben von Bitelementen , und es gibt potenziell schwerwiegende Thread-Sicherheitsprobleme in Bezug auf Bitfelder (insbesondere auf Multiprozessorsystemen), da die meisten Maschinen keine beliebigen Sätze von Bits im Speicher manipulieren können. sondern muss stattdessen ganze Wörter laden und speichern. zB wäre das Folgende trotz der Verwendung eines Mutex nicht threadsicher

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

Und wenn Sie mehr Gründe brauchen, um Bitfelder nicht zu verwenden, wird Raymond Chen Sie vielleicht in seinem Beitrag The Old New Thing Post überzeugen : 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;
}

Es ist cool, sie in einen Namespace zu stellen. Wenn sie in Ihrer CPP- oder Header-Datei deklariert sind, werden ihre Werte eingefügt. Sie können diese Werte einschalten, aber die Kopplung wird dadurch leicht erhöht.

Ah ja: Entfernen Sie das statische Schlüsselwort . static ist in C ++ veraltet, wenn Sie wie Sie verwendet werden. Wenn uint8 ein Build-Typ ist, benötigen Sie dies nicht, um dies in einem Header zu deklarieren, der von mehreren Quellen desselben Moduls enthalten wird. Am Ende sollte der Code sein:

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

Das Problem dieses Ansatzes ist, dass Ihr Code den Wert Ihrer Konstanten kennt, was die Kopplung geringfügig erhöht.

Aufzählung

Das gleiche wie const int, mit einer etwas stärkeren Eingabe.

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

Sie verschmutzen jedoch immer noch den globalen Namespace. Übrigens ... Entfernen Sie das typedef . Sie arbeiten in C ++. Diese Typedefs von Aufzählungen und Strukturen 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 Aufzählung den globalen Namespace. Wenn Sie diese Aufzählung in einen Namespace einfügen, haben Sie Folgendes:

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 möchten (dh die Werte der Konstanten ausblenden und sie so nach Bedarf ändern können, ohne dass eine vollständige Neukompilierung erforderlich ist), können Sie die Ints im Header als extern und in der CPP-Datei als konstant 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;
}

Sie können diese Konstanten jedoch nicht einschalten. Also, am Ende, wähle dein Gift ... :-p

paercebal
quelle
5
Warum denken Sie, sind Bitfelder langsam? Haben Sie tatsächlich Code mit ihm und einer anderen Methode profiliert? Selbst wenn dies der Fall ist, kann Klarheit wichtiger sein als Geschwindigkeit, wodurch "Verwenden Sie das nie" etwas vereinfacht wird.
wnoise
"static const uint8 xNew;" ist nur redundant, da in C ++ const Variablen mit Namespace-Gültigkeitsbereich standardmäßig interne Verknüpfungen verwendet werden. Entfernen Sie "const" und es hat eine externe Verknüpfung. Außerdem "enum {...} RecordType;" deklariert eine globale Variable mit dem Namen "RecordType", deren Typ eine anonyme Aufzählung ist.
bk1e
onebyone: Erstens war der Hauptgrund, dass der Gewinn (einige Bytes, falls vorhanden) durch den Verlust überschattet wurde (langsamerer Zugriff, sowohl Lesen als auch Schreiben) ...
paercebal
3
onebyone: Zweitens ist der gesamte Code, den ich bei der Arbeit oder zu Hause produziere, von Natur aus threadsicher. Das geht ganz einfach: Keine Globals, keine Statics, nicht zwischen Threads geteilt, es sei denn, sie sind durch Sperren geschützt. Die Verwendung dieser Redewendung würde diese grundlegende Thread-Sicherheit beeinträchtigen. Und wofür? Ein paar Bytes vielleicht ? ... :-) ...
paercebal
Verweis auf Raymond Chens Artikel über die versteckten Kosten von Bitfields hinzugefügt.
Paercebal
30

Haben Sie std :: bitset ausgeschlossen? Sätze von Flags ist, wofür es ist. Machen

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 für Bitset eine Reihe von Operatorüberladungen gibt, können Sie dies jetzt 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ürde mich über Korrekturen freuen, da ich dies nicht getestet habe. Sie können die Bits auch nach Index referenzieren. Im Allgemeinen ist es jedoch am besten, nur einen Satz von Konstanten zu definieren, und RecordType-Konstanten sind wahrscheinlich nützlicher.

Angenommen, Sie haben Bitset ausgeschlossen, stimme ich für die Aufzählung .

Ich kaufe nicht, dass das Casting der Aufzählungen ein schwerwiegender Nachteil ist - OK, es ist also etwas laut, und das Zuweisen eines Wertes außerhalb des Bereichs zu einer Aufzählung ist ein undefiniertes Verhalten, sodass es theoretisch möglich ist, sich auf einem ungewöhnlichen C ++ in den Fuß zu schießen Implementierungen. Aber wenn Sie es nur tun, wenn es nötig ist (wenn Sie von int zu enum iirc wechseln), ist es ganz normaler Code, den die Leute zuvor gesehen haben.

Ich bin auch über die Platzkosten der Aufzählung zweifelhaft. uint8-Variablen und -Parameter verwenden wahrscheinlich nicht weniger Stapel als Ints, sodass nur die Speicherung in Klassen von Bedeutung ist. Es gibt einige Fälle, in denen das Packen mehrerer Bytes in eine Struktur gewinnt (in diesem Fall können Sie Aufzählungen in den uint8-Speicher und aus diesem heraus umwandeln), aber normalerweise wird das Auffüllen den Vorteil trotzdem zunichte machen.

Die Aufzählung hat also keine Nachteile im Vergleich zu den anderen und bietet Ihnen als Vorteil ein wenig Typensicherheit (Sie können keinen zufälligen ganzzahligen Wert zuweisen, ohne explizit zu wirken) und saubere Möglichkeiten, auf alles zu verweisen.

Für die Präferenz würde ich übrigens auch das "= 2" in die Aufzählung setzen. Es ist nicht notwendig, aber ein "Prinzip des geringsten Erstaunens" legt nahe, dass alle 4 Definitionen gleich aussehen sollten.

Steve Jessop
quelle
1
Eigentlich habe ich Bitset überhaupt nicht berücksichtigt. Ich bin mir jedoch nicht sicher, ob es gut wäre. Mit Bitset muss ich Bits als 1, 2, 3, 4 adressieren, was den Code weniger lesbar machen würde - was bedeutet, dass ich wahrscheinlich eine Aufzählung verwenden würde, um die Bits zu "benennen". Könnte aber platzsparend sein. Vielen Dank.
Milan Babuškov
Milan, Sie müssen die Bits nicht mit einer Aufzählung "benennen", sondern können einfach die vordefinierten Bits wie oben gezeigt verwenden. Wenn Sie Bit eins anstelle von my_bitset.flip (1) aktivieren möchten, würden Sie my_bitset | = xNew;
Moswald
das richtet sich weniger an dich als an die STL, aber: ich muss wirklich fragen: warum würdest du das verwenden bitset? Normalerweise bedeutet dies longfür jedes Element einen (in meiner Implementierung iirc; ja, wie verschwenderisch) oder ähnlichen Integraltyp. Warum also nicht einfach unverhüllte Integrale verwenden? (oder heutzutage ohne constexprSpeicherplatz)
underscore_d
[Timeout bearbeiten] ... aber dann habe ich bitsetdie Gründe für die Klasse nie wirklich verstanden, abgesehen von dem , was in den umgebenden Diskussionen über 'ugh immer wieder als Unterströmung erscheint, müssen wir die unappetitlichen Wurzeln der Sprache auf niedriger Ebene vertuschen '
underscore_d
" uint8Variablen und Parameter werden wahrscheinlich nicht weniger Stapel verwenden als ints" ist falsch. Wenn Sie eine CPU mit 8-Bit-Registern haben, intbenötigen Sie mindestens 2 Register, während Sie uint8_tnur 1 benötigen. Daher benötigen Sie mehr Stapelspeicher, da Sie mit größerer Wahrscheinlichkeit keine Register mehr haben (was auch langsamer ist und die Codegröße erhöhen kann). abhängig vom Befehlssatz)). (Sie haben einen Typ, sollte es uint8_tnicht sein uint8)
12431234123412341234123
5

Verwenden Sie nach Möglichkeit KEINE Makros. Sie werden nicht allzu sehr bewundert, wenn es um modernes C ++ geht.

INS
quelle
4
Wahr. Was ich an Makros selbst hasse, ist, dass man sie nicht betreten kann, wenn sie falsch sind.
Carl
Ich stelle mir vor, das könnte im Compiler behoben werden.
Celticminstrel
4

Aufzählungen wären angemessener, da sie "Bedeutung für die Bezeichner" sowie Typensicherheit bieten. Sie können deutlich erkennen, dass "xDeleted" vom Typ "RecordType" ist und "Typ eines Datensatzes" (wow!) Auch nach Jahren darstellt. Consts würden dafür Kommentare erfordern, außerdem müssten sie im Code auf und ab gehen.

hayalci
quelle
4

Mit 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 unbedingt - aber Sie müssen an den Speicherorten 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 gießen, wenn ich bitweise operieren möchte.

Sie können Operatoren erstellen, um die Schmerzen zu lindern:

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

Mit const denke ich, dass ich auch die Typensicherheit verliere, da ein zufälliges uint8 versehentlich hineinkommen könnte.

Dasselbe kann bei jedem dieser Mechanismen passieren: Bereichs- und Wertprüfungen sind normalerweise orthogonal zur Typensicherheit (obwohl benutzerdefinierte Typen - dh Ihre eigenen Klassen - "Invarianten" über ihre Daten erzwingen können). Bei Aufzählungen kann der Compiler einen größeren Typ auswählen, um die Werte zu hosten, und eine nicht initialisierte, beschädigte oder einfach falsch gesetzte Aufzählungsvariable könnte ihr Bitmuster immer noch als eine Zahl interpretieren, die Sie nicht erwarten würden - im Vergleich zu einer anderen ungleich die Aufzählungskennungen, eine beliebige Kombination davon und 0.

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

Nun, am Ende funktioniert das bewährte bitweise ODER von Aufzählungen im C-Stil ziemlich gut, wenn Sie Bitfelder und benutzerdefinierte Operatoren im Bild haben. Sie können Ihre Robustheit mit einigen benutzerdefinierten Validierungsfunktionen und Zusicherungen wie in der Antwort von mat_geek weiter verbessern. Techniken, die häufig gleichermaßen für den Umgang mit Zeichenfolgen, Int, Doppelwerten 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 - so schön sie auch sind - sind immer noch chaotischer und schwerer zu finden als bitweise ORs.

Übrigens / - das Argument über die ziemlich schwache IMHO der Thread-Sicherheit - am besten als Hintergrundüberlegung in Erinnerung behalten, anstatt zu einer dominanten entscheidungsgetriebenen Kraft zu werden; Das Teilen eines Mutex über die Bitfelder hinweg ist eine wahrscheinlichere Vorgehensweise, auch wenn sie sich ihrer Packung nicht bewusst sind (Mutexe sind relativ umfangreiche Datenelemente - ich muss mir wirklich Sorgen um die Leistung machen, um in Betracht zu ziehen, mehrere Mutexe für Mitglieder eines Objekts zu haben, und ich würde genau hinschauen genug, um zu bemerken, dass es sich um Bitfelder handelte). Jeder Typ mit Unterwortgröße kann das gleiche Problem haben (z uint8_t. B. a ). Auf jeden Fall können Sie atomare Vergleichs- und Austauschvorgänge ausprobieren, wenn Sie eine höhere Parallelität wünschen.

Tony Delroy
quelle
1
+1 Gut. Aber das operator|sollte unsigned intvor der Anweisung in einen Integer-Typ ( ) umgewandelt werden |. operator|Andernfalls ruft sich der Wille rekursiv auf und verursacht einen Laufzeitstapelüberlauf. Ich schlage vor : return RecordType( unsigned(lhs) | unsigned(rhs) );. Cheers
Olibre
3

Selbst wenn Sie 4 Byte zum Speichern einer Aufzählung verwenden müssen (ich bin mit C ++ nicht so vertraut - ich weiß, dass Sie den zugrunde liegenden Typ in C # angeben können), lohnt es sich dennoch, Aufzählungen zu verwenden.

In der heutigen Zeit von Servern mit GB Arbeitsspeicher spielen Dinge wie 4 Byte gegenüber 1 Byte Speicher auf Anwendungsebene im Allgemeinen keine Rolle. Wenn in Ihrer speziellen Situation die Speichernutzung so wichtig ist (und Sie C ++ nicht dazu bringen können, ein Byte zum Sichern der Aufzählung zu verwenden), können Sie die Route 'static const' in Betracht ziehen.

Am Ende des Tages müssen Sie sich fragen, ob es sich lohnt, 'statische Konstante' für die 3 Byte Speichereinsparung für Ihre Datenstruktur zu verwenden.

Beachten Sie noch etwas anderes: IIRC: Auf x86 sind Datenstrukturen 4-Byte-ausgerichtet. Wenn Sie also keine Elemente mit Byte-Breite in Ihrer Datensatzstruktur haben, spielt dies möglicherweise keine Rolle. Testen Sie dies und stellen Sie sicher, dass dies der Fall ist, bevor Sie einen Kompromiss hinsichtlich der Wartbarkeit für Leistung / Platz eingehen.

Jonathan Rupp
quelle
Sie können den zugrunde liegenden Typ in C ++ ab Sprachversion C ++ 11 angeben. Bis dahin war es meiner Meinung nach "zumindest groß genug, um es zu speichern und als Bitfeld für alle angegebenen Enumeratoren zu verwenden, aber wahrscheinlich, intwenn es nicht zu klein ist". [Wenn Sie den zugrunde liegenden Typ in C ++ 11 nicht angeben, wird das Legacy-Verhalten verwendet. Umgekehrt wird der enum classzugrunde liegende Typ von C ++ 11 explizit standardmäßig verwendet, intsofern nicht anders angegeben.]
Justin Time - Monica
3

Wenn Sie die Typensicherheit von Klassen mit der Bequemlichkeit der Aufzählungssyntax und der Bitprüfung wünschen, sollten Sie Safe Labels in C ++ in Betracht ziehen . Ich habe mit dem Autor gearbeitet und er ist ziemlich schlau.

Aber Vorsicht. Am Ende verwendet dieses Paket Vorlagen und Makros!

Don Wakefield
quelle
Sieht nach Overkill für meine kleine App aus. aber es scheint eine gute Lösung zu sein.
Milan Babuškov
2

Müssen Sie die Flag-Werte tatsächlich als konzeptionelles Ganzes weitergeben, oder haben Sie viel Code pro Flag? In jedem Fall könnte es klarer sein, dies als Klasse oder Struktur von 1-Bit-Bitfeldern zu haben:

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

Dann könnte Ihre Datensatzklasse eine Mitgliedsvariable struct RecordFlag haben, Funktionen können Argumente vom Typ struct RecordFlag annehmen usw. Der Compiler sollte die Bitfelder zusammenfügen, um Platz zu sparen.

wnoise
quelle
Mal als Ganzes, mal als Flagge. Außerdem muss ich testen, ob ein bestimmtes Flag gesetzt ist (wenn ich es als Ganzes übergebe).
Milan Babuškov
Nun, wenn getrennt, fragen Sie einfach nach einem Int. Wenn Sie zusammen sind, übergeben Sie die Struktur.
wnoise
Es wird nicht besser sein. Der Zugriff auf Bitfelder ist langsamer als alles andere.
Paercebal
"Ja wirklich?" Sie denken, der Compiler generiert zum Testen von Bitfeldern erheblich anderen Code als das manuelle Bit-Twiddling? Und dass es deutlich langsamer wird? Warum? Das einzige, was Sie idiomatisch nicht so einfach tun können, ist, mehrere Flags gleichzeitig zu maskieren.
wnoise
Wenn ich einen einfachen Lesetest durchführe, erhalte ich 5,50-5,58 Sekunden für die Bitmaskierung gegenüber 5,45-5,59 Sekunden für den Bitfeldzugriff. Ziemlich ununterscheidbar.
wnoise
2

Ich würde wahrscheinlich keine Aufzählung für solche Dinge verwenden, bei denen die Werte miteinander kombiniert werden können. Typischerweise schließen sich Aufzählungen gegenseitig aus.

Unabhängig von der verwendeten Methode verwenden Sie stattdessen diese Syntax für die tatsächlichen Werte, um klarer zu machen, dass es sich um Werte handelt, die Bits sind, die miteinander kombiniert werden können:

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

Wenn Sie eine Linksverschiebung verwenden, können Sie anzeigen, dass jeder Wert ein einzelnes Bit sein soll. Es ist weniger wahrscheinlich, dass später jemand etwas falsch macht, z. B. einen neuen Wert hinzufügt und ihm einen Wert von 9 zuweist.


quelle
1
Dafür gibt es genügend Präzedenzfälle, insbesondere bei Konstanten für ioctl (). Ich bevorzuge jedoch Hex-Konstanten: 0x01, 0x02, 0x04, 0x08, 0x10, ...
Jonathan Leffler
2

Stellen Sie diese Fragen basierend auf KISS , hoher Kohäsion und geringer Kopplung -

  • Wer muss es wissen? meine Klasse, meine Bibliothek, andere Klassen, andere Bibliotheken, Dritte
  • Welche Abstraktionsebene muss ich bereitstellen? Versteht der Verbraucher Bitoperationen?
  • Muss ich eine Schnittstelle von VB / C # usw. herstellen?

Es gibt ein großartiges Buch " Large-Scale C ++ Software Design ", das Basistypen extern fördert, wenn Sie eine andere Abhängigkeit von Header-Datei / Schnittstelle vermeiden können, die Sie versuchen sollten.

Titanae
quelle
1
a) 5-6 Klassen. b) nur ich, es ist ein Ein-Mann-Projekt c) keine Schnittstelle
Milan Babuškov
2

Wenn Sie Qt verwenden, sollten Sie nach QFlags suchen . Die QFlags-Klasse bietet eine typsichere Möglichkeit zum Speichern von ODER-Kombinationen von Enum-Werten.

Thomas Koschel
quelle
Nein, kein Qt. Eigentlich ist es ein wxWidgets-Projekt.
Milan Babuškov
0

Ich würde lieber mit gehen

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

Einfach weil:

  1. Es ist sauberer und macht den Code lesbar und wartbar.
  2. Es gruppiert die Konstanten logisch.
  3. Programmierer Zeit ist wichtiger, es sei denn , Ihre Aufgabe ist , diese 3 Bytes zu speichern.
Vivek
quelle
Nun, ich könnte leicht eine Million Instanzen des Klassenrekords haben, also könnte es wichtig sein. OTOH, das ist nur ein Unterschied zwischen 1 MB und 4 MB, also sollte ich mir vielleicht keine Sorgen machen.
Milan Babuškov
@Vivek: Haben Sie die Beschränkung der Ganzzahlbreite berücksichtigt? Insbesondere vor C ++ 11.
user2672165
0

Nicht, dass ich gerne alles überentwickeln würde, aber manchmal kann es in diesen Fällen sinnvoll sein, eine (kleine) Klasse zu erstellen, um diese Informationen zu kapseln. Wenn Sie eine Klasse RecordType erstellen, hat diese möglicherweise folgende Funktionen:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

etc ... (oder was auch immer Konvention passt)

Es könnte Kombinationen validieren (falls nicht alle Kombinationen zulässig sind, z. B. wenn "neu" und "gelöscht" nicht beide gleichzeitig festgelegt werden können). Wenn Sie nur Bitmasken usw. verwendet haben, muss der Code, der den Status festlegt, validiert werden. Eine Klasse kann diese Logik auch kapseln.

Die Klasse bietet Ihnen möglicherweise auch die Möglichkeit, jedem Status aussagekräftige Protokollierungsinformationen hinzuzufügen. Sie können eine Funktion hinzufügen, um eine Zeichenfolgendarstellung des aktuellen Status usw. zurückzugeben (oder die Streaming-Operatoren '<<' verwenden).

Wenn Sie sich Sorgen um den Speicher machen, kann es sein, dass die Klasse nur ein 'char'-Datenelement hat. Nehmen Sie also nur eine kleine Menge Speicher (vorausgesetzt, es ist nicht virtuell). Abhängig von der Hardware usw. können natürlich Ausrichtungsprobleme auftreten.

Sie könnten die tatsächlichen Bitwerte für den Rest der 'Welt' nicht sichtbar haben, wenn sie sich in einem anonymen Namespace innerhalb der cpp-Datei und nicht in der Header-Datei befinden.

Wenn Sie feststellen, dass der Code, der die Aufzählung / # define / bitmask usw. verwendet, viel 'Support'-Code für ungültige Kombinationen, Protokollierung usw. enthält, ist die Kapselung in einer Klasse möglicherweise eine Überlegung wert. Natürlich sind einfache Probleme meistens mit einfachen Lösungen besser dran ...

Benjamin
quelle
Leider muss sich die Deklaration in einer .h-Datei befinden, da sie projektübergreifend verwendet wird (von einigen 5-6 Klassen verwendet).
Milan Babuškov