Domanda

In un progetto C ++ su cui sto lavorando, ho un tipo di valore flag che può avere quattro valori. Queste quattro bandiere possono essere combinate. Le bandiere descrivono i record nel database e possono essere:

  • nuovo record
  • record eliminato
  • record modificato
  • record esistente

Ora, per ogni record, desidero conservare questo attributo, in modo da poter usare un enum:

enum { xNew, xDeleted, xModified, xExisting }

Tuttavia, in altri punti del codice, devo selezionare quali record devono essere visibili all'utente, quindi vorrei poterlo passare come singolo parametro, come:

showRecords(xNew | xDeleted);

Quindi, sembra che io abbia tre possibili approcci:

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

o

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

o

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

I requisiti di spazio sono importanti (byte vs int) ma non cruciali. Con definisce perdo la sicurezza del tipo e con enum perdo un po 'di spazio (numeri interi) e probabilmente devo lanciare quando voglio fare un'operazione bit a bit. Con const Penso di perdere anche la sicurezza dei tipi poiché un uint8 casuale potrebbe entrare per errore.

C'è un altro modo più pulito?

In caso contrario, cosa useresti e perché?

P.S. Il resto del codice è C ++ moderno piuttosto pulito senza #define s, e ho usato spazi dei nomi e modelli in pochi spazi, quindi neanche quelli sono fuori discussione.

È stato utile?

Soluzione

Combina le strategie per ridurre gli svantaggi di un singolo approccio. Lavoro nei sistemi embedded, quindi la seguente soluzione si basa sul fatto che gli operatori interi e bit a bit sono ampli & Veloci, con poca memoria; basso utilizzo del flash.

Posiziona l'enum in uno spazio dei nomi per impedire alle costanti di inquinare lo spazio dei nomi globale.

namespace RecordType {

Un enum dichiara e definisce un tempo di compilazione verificato digitato. Utilizzare sempre il controllo del tipo di tempo di compilazione per assicurarsi che gli argomenti e le variabili ricevano il tipo corretto. Non è necessario il typedef in C ++.

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

Crea un altro membro per uno stato non valido. Questo può essere utile come codice di errore; ad esempio, quando si desidera restituire lo stato ma l'operazione I / O non riesce. È anche utile per il debug; usalo negli elenchi di inizializzazione e nei distruttori per sapere se usare il valore della variabile.

xInvalid = 16 };

Considera che hai due scopi per questo tipo. Per tenere traccia dello stato corrente di un record e creare una maschera per selezionare i record in determinati stati. Creare una funzione incorporata per verificare se il valore del tipo è valido per il proprio scopo; come indicatore di stato vs maschera di stato. Questo catturerà i bug poiché typedef è solo un int e un valore come 0xDEADBEEF potrebbe essere nella tua variabile attraverso variabili non inizializzate o errate.

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

Aggiungi una direttiva using se vuoi usare il tipo spesso.

using RecordType ::TRecordType ;

Le funzioni di controllo del valore sono utili negli asserti per intrappolare valori errati non appena vengono utilizzati. Più rapidamente si rileva un bug durante l'esecuzione, minore è il danno che può causare.

Ecco alcuni esempi per mettere tutto insieme.

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

L'unico modo per garantire la corretta sicurezza del valore è utilizzare una classe dedicata con sovraccarichi dell'operatore e che viene lasciata come esercizio per un altro lettore.

Altri suggerimenti

Dimentica le definizioni

Inquineranno il tuo codice.

campi di bit?

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

Non usarlo mai . Sei più interessato alla velocità che all'economizzazione di 4 pollici. L'uso dei campi bit è in realtà più lento dell'accesso a qualsiasi altro tipo.

Tuttavia, i membri bit nelle strutture presentano svantaggi pratici. Innanzitutto, l'ordinamento dei bit in memoria varia da compilatore a compilatore. Inoltre, molti compilatori popolari generano codice inefficiente per la lettura e la scrittura dei membri bit e vi sono potenzialmente problemi di sicurezza dei thread relativi ai campi bit (in particolare sui sistemi multiprocessore) a causa di il fatto che la maggior parte delle macchine non può manipolare insiemi arbitrari di bit in memoria, ma deve invece caricare e memorizzare intere parole. ad esempio, quanto segue non sarebbe sicuro per i thread, nonostante l'uso di un mutex

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

E se hai bisogno di più motivi per non utilizzare i bitfield, forse Raymond Chen ti convincerà nella sua The Old New Thing Post: L'analisi costi-benefici di campi di bit per una raccolta di booleani all'indirizzo 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;
}

Metterli in uno spazio dei nomi è bello. Se sono dichiarati nel tuo CPP o file di intestazione, i loro valori saranno in linea. Sarai in grado di utilizzare l'interruttore su questi valori, ma aumenterà leggermente l'accoppiamento.

Ah, sì: rimuovi la parola chiave statica . static è deprecato in C ++ quando usato come fai tu, e se uint8 è un tipo buildin, non ti servirà per dichiararlo in un'intestazione inclusa da più fonti dello stesso modulo. Alla fine, il codice dovrebbe essere:

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

Il problema di questo approccio è che il codice conosce il valore delle costanti, il che aumenta leggermente l'accoppiamento.

enum

Lo stesso di const int, con una digitazione leggermente più forte.

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

Stanno comunque inquinando lo spazio dei nomi globale. A proposito ... Rimuovi il typedef . Stai lavorando in C ++. Quei dattiloscritti di enumerazioni e strutture stanno inquinando il codice più di ogni altra cosa.

Il risultato è kinda:

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

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

Come vedi, il tuo enum sta inquinando lo spazio dei nomi globale. Se metti questa enum in uno spazio dei nomi, avrai qualcosa del tipo:

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?

Se si desidera ridurre l'accoppiamento (ovvero essere in grado di nascondere i valori delle costanti, e quindi modificarli come desiderato senza bisogno di una completa ricompilazione), è possibile dichiarare gli in come esterni nell'intestazione e come costanti in il file CPP, come nell'esempio seguente:

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

E

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

Tuttavia non sarai in grado di usare l'interruttore su quelle costanti. Quindi alla fine, scegli il tuo veleno ... :-p

Hai escluso std :: bitset? Gli insiemi di bandiere servono a questo scopo. Fare

typedef std::bitset<4> RecordType;

poi

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

Poiché ci sono un sacco di sovraccarichi dell'operatore per il bitset, ora puoi farlo

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

O qualcosa di molto simile a questo - apprezzerei qualsiasi correzione poiché non l'ho provato. Puoi anche fare riferimento ai bit per indice, ma in genere è meglio definire solo un set di costanti e le costanti RecordType sono probabilmente più utili.

Supponendo che tu abbia escluso il bitset, voto per il enum .

Non compro che lanciare gli enum sia un grave svantaggio - OK, quindi è un po 'rumoroso, e assegnare un valore fuori portata a un enum è un comportamento indefinito, quindi teoricamente è possibile spararsi al piede alcune insolite implementazioni C ++. Ma se lo fai solo quando necessario (che è quando vai da int a enum iirc), è un codice perfettamente normale che le persone hanno visto prima.

Sono anche incerto su qualsiasi costo di spazio dell'enum. Le variabili e i parametri uint8 probabilmente non useranno meno stack di ints, quindi conta solo l'archiviazione nelle classi. Ci sono alcuni casi in cui vincerà il confezionamento di più byte in una struttura (nel qual caso è possibile eseguire il cast di enumerazioni dentro e fuori dalla memoria di uint8), ma normalmente il riempimento ucciderà comunque il vantaggio.

Quindi l'enum non ha svantaggi rispetto agli altri, e come vantaggio ti dà un po 'di sicurezza del tipo (non puoi assegnare un valore intero casuale senza esplicitamente il cast) e modi chiari di riferirti a tutto.

Per preferenza metterei anche " = 2 " nell'enum, a proposito. Non è necessario, ma un & Quot; principio del minimo stupore & Quot; suggerisce che tutte e 4 le definizioni dovrebbero avere lo stesso aspetto.

Ecco un paio di articoli su const vs. macros vs. enums:

Costanti simboliche
Costanti di enumerazione contro costante Oggetti

Penso che dovresti evitare le macro soprattutto perché hai scritto che la maggior parte del tuo nuovo codice è in C ++ moderno.

Se possibile NON utilizzare le macro. Non sono troppo ammirati quando si tratta del C ++ moderno.

Gli enum sarebbero più appropriati in quanto forniscono " significato per gli identificatori " così come la sicurezza del tipo. Puoi chiaramente dire & Quot; xDeleted & Quot; è di " RecordType " e che rappresentano " tipo di record " (wow!) anche dopo anni. I costi richiederebbero commenti per questo, inoltre richiederebbero di andare su e giù nel codice.

  

Con definisce perdo la sicurezza del tipo

Non necessariamente ...

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

e con enum perdo dello spazio (numeri interi)

Non necessariamente - ma devi essere esplicito nei punti di archiviazione ...

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

e probabilmente devo lanciare quando voglio fare un'operazione bit a bit.

Puoi creare operatori per eliminare questo problema:

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

Con const penso di perdere anche la sicurezza dei tipi poiché un uint8 casuale potrebbe entrare per errore.

Lo stesso può accadere con uno di questi meccanismi: i controlli di intervallo e valore sono normalmente ortogonali alla sicurezza dei tipi (sebbene i tipi definiti dall'utente - cioè le tue classi - possano far rispettare " invariants " about i loro dati). Con enums, il compilatore è libero di scegliere un tipo più grande per ospitare i valori e una variabile enum non inizializzata, corrotta o semplicemente errata potrebbe finire per interpretare il suo modello di bit come un numero che non ti aspetteresti - confrontando ineguale con uno qualsiasi gli identificatori di enumerazione, qualsiasi combinazione di essi e 0.

  

C'è un altro modo più pulito? / In caso contrario, cosa useresti e perché?

Bene, alla fine il comprovato OR di bitumerazione in stile C delle enumerazioni funziona abbastanza bene una volta che hai campi di bit e operatori personalizzati nella foto. Puoi migliorare ulteriormente la tua solidità con alcune funzioni e asserzioni personalizzate di convalida come nella risposta di mat_geek; tecniche spesso ugualmente applicabili alla gestione di stringhe, int, doppi valori ecc.

Potresti sostenere che questo è " clean " ;:

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

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

Sono indifferente: i bit di dati sono più stretti ma il codice cresce in modo significativo ... dipende da quanti oggetti hai, e i lamdbas - per quanto siano belli - sono ancora più sporchi e più difficili da ottenere rispetto agli OR bit a bit .

A proposito / - l'argomento sull'IMHO piuttosto debole della sicurezza dei thread - è meglio ricordarlo come considerazione di fondo piuttosto che diventare una forza trainante decisionale dominante; condividere un mutex attraverso i bitfield è una pratica più probabile anche se ignari del loro impacchettamento (i mutex sono membri di dati relativamente voluminosi - devo essere veramente preoccupato per le prestazioni per considerare di avere più mutex sui membri di un oggetto, e guarderei attentamente abbastanza da notare che erano piccoli campi). Qualsiasi tipo di dimensione di una parola secondaria potrebbe avere lo stesso problema (ad esempio un uint8_t). Ad ogni modo, potresti provare operazioni atomiche di confronto e scambio se sei alla disperata ricerca di una maggiore concorrenza.

Anche se devi usare 4 byte per memorizzare un enum (non ho molta familiarità con C ++ - so che puoi specificare il tipo sottostante in C #), ne vale comunque la pena - usa enums.

Al giorno d'oggi dei server con GB di memoria, cose come 4 byte contro 1 byte di memoria a livello di applicazione in generale non contano. Naturalmente, se nella tua particolare situazione, l'uso della memoria è così importante (e non riesci a far sì che C ++ usi un byte per sostenere l'enum), allora puoi considerare il percorso 'const statico'.

Alla fine della giornata, devi chiederti, vale la pena il colpo di manutenzione dell'utilizzo di 'const statica' per i 3 byte di risparmio di memoria per la tua struttura dati?

Qualcos'altro da tenere a mente - IIRC, su x86, le strutture di dati sono allineate a 4 byte, quindi a meno che tu non abbia un numero di elementi di larghezza di byte nella tua struttura 'record', potrebbe non avere importanza. Prova e assicurati che lo faccia prima di fare un compromesso sulla manutenibilità per prestazioni / spazio.

Se si desidera il tipo di sicurezza delle classi, con la comodità della sintassi di enumerazione e del controllo dei bit, considerare Etichette sicure in C ++ . Ho lavorato con l'autore ed è piuttosto intelligente.

Attenzione, però. Alla fine, questo pacchetto utilizza i modelli e macro!

Hai davvero bisogno di passare intorno ai valori della bandiera come un insieme concettuale o avrai un sacco di codice per bandiera? Ad ogni modo, penso che avere questo come classe o struttura di bitfield a 1 bit potrebbe effettivamente essere più chiaro:

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

Quindi la tua classe di record potrebbe avere una variabile membro RecordFlag struct, le funzioni possono prendere argomenti di tipo struct RecordFlag, ecc. Il compilatore dovrebbe mettere insieme i bitfield, risparmiando spazio.

Probabilmente non userei un enum per questo genere di cose in cui i valori possono essere combinati insieme, più tipicamente gli enum sono stati reciprocamente esclusivi.

Ma qualunque sia il metodo utilizzato, per chiarire che si tratta di valori che sono bit che possono essere combinati insieme, utilizzare invece questa sintassi per i valori effettivi:

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

Usando uno spostamento a sinistra aiuta a indicare che ogni valore deve essere un singolo bit, è meno probabile che in seguito qualcuno faccia qualcosa di sbagliato come aggiungere un nuovo valore e assegnargli qualcosa con un valore di 9.

Basato su KISS , alta coesione e basso accoppiamento , fai queste domande -

  • Chi deve sapere? la mia classe, la mia biblioteca, altre classi, altre biblioteche, terze parti
  • Quale livello di astrazione devo fornire? Il consumatore comprende le operazioni sui bit.
  • Dovrò interfacciarmi da VB / C # ecc?

Esiste un ottimo libro " Design del software C ++ su larga scala " ;, questo promuove i tipi di base esternamente, se puoi evitare un altro file header / dipendenza dall'interfaccia che dovresti provare.

Se stai usando Qt dovresti dare un'occhiata a QFlags . La classe QFlags offre un modo sicuro per la memorizzazione di combinazioni OR di valori enum.

Preferirei andare con

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

Semplicemente perché:

  1. È più pulito e rende il codice leggibile e gestibile.
  2. Raggruppa logicamente le costanti.
  3. Il tempo del programmatore è più importante, a meno che il lavoro non sia per salvare quei 3 byte.

Non che mi piace progettare troppo tutto, ma a volte in questi casi può valere la pena creare una (piccola) classe per incapsulare queste informazioni. Se crei un RecordType di classe, potrebbe avere funzioni come:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

ecc ... (o qualunque altra convenzione adatta)

Potrebbe convalidare le combinazioni (nel caso in cui non tutte le combinazioni siano legali, ad esempio se "nuovo" e "eliminato" non possono essere impostati contemporaneamente). Se hai appena usato maschere di bit ecc., Il codice che imposta lo stato deve essere convalidato, una classe può incapsulare anche quella logica.

La classe può anche darti la possibilità di allegare informazioni di registrazione significative a ciascuno stato, puoi aggiungere una funzione per restituire una rappresentazione in forma di stringa dello stato corrente ecc. (o usare < & degli operatori di streaming)! lt; ').

Nonostante ciò, se sei preoccupato per l'archiviazione, potresti comunque avere la classe con solo un membro di dati 'char', quindi prendi solo una piccola quantità di spazio di archiviazione (supponendo che non sia virtuale). Naturalmente a seconda dell'hardware ecc. Potresti avere problemi di allineamento.

Potresti avere i valori di bit effettivi non visibili al resto del 'mondo' se si trovano in uno spazio dei nomi anonimo all'interno del file cpp piuttosto che nel file di intestazione.

Se trovi che il codice che usa l'enum / # define / bitmask etc ha molto codice 'support' per gestire combinazioni non valide, logging etc, allora vale la pena considerare l'incapsulamento in una classe. Naturalmente la maggior parte delle volte i problemi semplici sono migliori con soluzioni semplici ...

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top