Powinienem użyć #define, enum czy const?

125

W projekcie C ++, nad którym pracuję, mam wartość typu flagi, która może mieć cztery wartości. Te cztery flagi można łączyć. Flagi opisują rekordy w bazie danych i mogą być:

  • nowy rekord
  • usunięty rekord
  • zmodyfikowany rekord
  • istniejący rekord

Teraz dla każdego rekordu chcę zachować ten atrybut, więc mogę użyć wyliczenia:

enum { xNew, xDeleted, xModified, xExisting }

Jednak w innych miejscach w kodzie muszę wybrać, które rekordy mają być widoczne dla użytkownika, więc chciałbym móc przekazać to jako pojedynczy parametr, na przykład:

showRecords(xNew | xDeleted);

Wygląda więc na to, że mam trzy możliwe podejścia:

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

lub

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

lub

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

Wymagania dotyczące miejsca są ważne (bajt vs int), ale nie są kluczowe. W przypadku definicji tracę bezpieczeństwo typów, a wraz z nimi enumtracę trochę spacji (liczby całkowite) i prawdopodobnie muszę rzucać, gdy chcę wykonać operację bitową. Z constMyślę też stracić typu bezpieczeństwa, ponieważ losowa uint8może dostać się przez pomyłkę.

Czy jest jakiś inny czystszy sposób?

Jeśli nie, czego byś użył i dlaczego?

PS Reszta kodu jest raczej czystym, nowoczesnym C ++ bez #defines, a użyłem przestrzeni nazw i szablonów w kilku miejscach, więc to też nie jest wykluczone.

Milan Babuškov
źródło
„w przypadku wyliczenia tracę trochę spacji (liczby całkowite)”. Niekoniecznie. Zobacz stackoverflow.com/questions/366017/ ... i stackoverflow.com/questions/1113855/… (i -fshort-enum gcc . (Zakładam, że te odpowiedzi w C są nadal prawdziwe w C ++).
idbrii
@pydave Jeśli nie masz pewności co do zgodności C i C ++, uważam ten link za bardzo pomocny, zobacz na przykład enum david.tribble.com/text/cdiffs.htm#C99-enum-type
aka.nice
3
Jest to starszy temat z dużą liczbą głosów, czy jest powód, aby nie wspominać o klasach wyliczeniowych C ++ 11 dla tej sytuacji problemowej.
Brandin
Uwaga, enum RecordType : uint8_tłączy w sobie bezpieczeństwo typów enumz niewielkim rozmiarem uint8_t, chociaż nadal będziesz musiał zapewnić operatory bitowe.
Justin Time - Przywróć Monikę

Odpowiedzi:

88

Połącz strategie, aby zmniejszyć wady jednego podejścia. Pracuję w systemach wbudowanych, więc poniższe rozwiązanie opiera się na fakcie, że operatory liczb całkowitych i bitowych są szybkie, mają mało pamięci i zużycie pamięci flash.

Umieść wyliczenie w przestrzeni nazw, aby zapobiec zanieczyszczaniu globalnej przestrzeni nazw przez stałe.

namespace RecordType {

Wyliczenie deklaruje i definiuje wpisany czas kompilacji. Zawsze używaj sprawdzania typu podczas kompilacji, aby upewnić się, że argumenty i zmienne mają prawidłowy typ. Nie ma potrzeby stosowania typedef w C ++.

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

Utwórz innego członka dla nieprawidłowego stanu. Może to być przydatne jako kod błędu; na przykład, gdy chcesz przywrócić stan, ale operacja we / wy zakończy się niepowodzeniem. Jest również przydatny do debugowania; użyj go w listach inicjalizacyjnych i destruktorach, aby wiedzieć, czy należy użyć wartości zmiennej.

xInvalid = 16 };

Weź pod uwagę, że masz dwa cele dla tego typu. Śledzenie aktualnego stanu rekordu i tworzenie maski do wybierania rekordów w określonych stanach. Utwórz funkcję wbudowaną, aby sprawdzić, czy wartość typu jest odpowiednia dla Twojego celu; jako znacznik stanu a maska ​​stanu. Spowoduje to wykrycie błędów, ponieważ typedefjest to po prostu an inti wartość, taka jak 0xDEADBEEFmoże znajdować się w zmiennej za pośrednictwem niezainicjowanych lub nieprawidłowo ustawionych zmiennych.

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

Dodaj usingdyrektywę, jeśli chcesz często używać tego typu.

using RecordType ::TRecordType ;

Funkcje sprawdzające wartości są przydatne w potwierdzeniach do wychwytywania złych wartości, gdy tylko zostaną użyte. Im szybciej złapiesz błąd podczas biegu, tym mniej szkód może on wyrządzić.

Oto kilka przykładów, aby to wszystko połączyć.

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

Jedynym sposobem na zapewnienie prawidłowego bezpieczeństwa wartości jest użycie dedykowanej klasy z przeciążeniami operatora i pozostawienie tego jako ćwiczenie dla innego czytelnika.

mat_geek
źródło
1
Przeważnie dobra odpowiedź - ale pytanie stanowi, że flagi można łączyć, a funkcja IsValidState () nie pozwala na ich łączenie.
Jonathan Leffler
3
@Jonathan Leffler: z miejsca, w którym stoję, wydaje mi się, że „IsValidState” nie powinien tego robić, „IsValidMask” jest.
João Portela
1
Czy pożądane jest, aby IsValidMasknie można było wybrać żadnego (tj. 0)?
Joachim Sauer
2
−1 Idea sprawdzania typów w czasie wykonywania jest ohydna.
Pozdrawiam i hth. - Alf
54

Zapomnij o definicjach

Zanieczyszczą twój kod.

bitfields?

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

Nigdy tego nie używaj . Bardziej interesuje Cię szybkość niż oszczędność 4 int. Korzystanie z pól bitowych jest w rzeczywistości wolniejsze niż dostęp do jakiegokolwiek innego typu.

Jednak elementy bitowe w strukturach mają praktyczne wady. Po pierwsze, kolejność bitów w pamięci różni się w zależności od kompilatora. Ponadto wiele popularnych kompilatorów generuje nieefektywny kod do odczytywania i zapisywania składowych bitów i istnieją potencjalnie poważne problemy z bezpieczeństwem wątków związane z polami bitowymi (szczególnie w systemach wieloprocesorowych), ponieważ większość maszyn nie może manipulować dowolnymi zestawami bitów w pamięci, ale zamiast tego musi ładować i przechowywać całe słowa. np. poniższe nie byłyby bezpieczne dla wątków, pomimo użycia muteksu

Źródło: http://en.wikipedia.org/wiki/Bit_field :

A jeśli potrzebujesz więcej powodów, aby nie używać pól bitowych, być może Raymond Chen przekona Cię w swoim poście The Old New Thing : Analiza kosztów i korzyści pól bitowych dla zbioru wartości logicznych pod adresem http://blogs.msdn.com/oldnewthing/ archiwum / 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;
}

Umieszczenie ich w przestrzeni nazw jest fajne. Jeśli są zadeklarowane w Twoim CPP lub pliku nagłówkowym, ich wartości zostaną wstawione. Będziesz mógł użyć przełącznika na tych wartościach, ale nieznacznie zwiększy to sprzężenie.

Ach, tak: usuń słowo kluczowe static . static jest przestarzałe w C ++, gdy jest używane tak jak ty, a jeśli uint8 jest typem kompilacji, nie będziesz tego potrzebować, aby zadeklarować to w nagłówku zawartym w wielu źródłach tego samego modułu. Na koniec kod powinien wyglądać następująco:

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

Problem z tym podejściem polega na tym, że twój kod zna wartość twoich stałych, co nieznacznie zwiększa sprzężenie.

enum

To samo co const int, z nieco silniejszym pisaniem.

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

Jednak nadal zanieczyszczają globalną przestrzeń nazw. Przy okazji ... Usuń typedef . Pracujesz w C ++. Te typy definicji wyliczeń i struktur zanieczyszczają kod bardziej niż cokolwiek innego.

Wynik jest taki:

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

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

Jak widzisz, Twoje wyliczenie zanieczyszcza globalną przestrzeń nazw. Jeśli umieścisz to wyliczenie w przestrzeni nazw, będziesz mieć coś takiego:

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?

Jeśli chcesz zmniejszyć sprzężenie (tj. Móc ukryć wartości stałych, a więc zmodyfikować je zgodnie z potrzebami bez konieczności pełnej rekompilacji), możesz zadeklarować ints jako extern w nagłówku i jako stałe w pliku CPP , jak w poniższym przykładzie:

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

I:

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

Nie będziesz jednak mógł użyć przełącznika na tych stałych. Więc w końcu wybierz swoją truciznę ... :-p

paercebal
źródło
5
Jak myślisz, dlaczego pola bitowe są powolne? Czy faktycznie profilowałeś kod przy użyciu tego i innej metody? Nawet jeśli tak jest, przejrzystość może być ważniejsza niż szybkość, co sprawia, że ​​„nigdy tego nie używaj” jest nieco uproszczone.
mądry,
"static const uint8 xNew;" jest zbędne tylko dlatego, że w C ++ const zmienne o zasięgu przestrzeni nazw są domyślnie łączone wewnętrznie. Usuń „const” i ma zewnętrzne powiązanie. Ponadto „enum {...} RecordType;” deklaruje zmienną globalną o nazwie „RecordType”, której typ jest anonimowym wyliczeniem.
bk1e
po pierwsze: po pierwsze, głównym powodem było to, że wzmocnienie (kilka bajtów, jeśli w ogóle) zostało przyćmione przez utratę (wolniejszy dostęp, zarówno do odczytu, jak i zapisu) ...
paercebal
3
onebyone: Po drugie, cały kod, który tworzę w pracy lub w domu, jest z natury bezpieczny wątkowo. To łatwe: bez globalnych, bez statycznych, bez współdzielenia między wątkami, chyba że są chronione blokadą. Użycie tego idiomu złamałoby podstawowe bezpieczeństwo wątków. I po co? Kilka bajtów może ... :-) ...?
paercebal
Dodano odniesienie do artykułu Raymonda Chena o ukrytych kosztach pól bitowych.
paercebal
30

Czy wykluczyłeś std :: bitset? Do tego służą zestawy flag. Robić

typedef std::bitset<4> RecordType;

następnie

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

Ponieważ istnieje wiele przeciążeń operatorów dla zestawu bitów, możesz teraz to zrobić

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

Lub coś bardzo podobnego - byłbym wdzięczny za wszelkie poprawki, ponieważ tego nie testowałem. Możesz także odwoływać się do bitów według indeksu, ale ogólnie najlepiej jest zdefiniować tylko jeden zestaw stałych, a stałe RecordType są prawdopodobnie bardziej przydatne.

Zakładając, że wykluczyłeś bitset, głosuję za wyliczeniem .

Nie kupuję tego, że rzucanie wyliczeń jest poważną wadą - OK, więc jest trochę hałaśliwe, a przypisanie wartości spoza zakresu do wyliczenia jest niezdefiniowanym zachowaniem, więc teoretycznie można strzelić sobie w stopę na jakimś nietypowym C ++ wdrożenia. Ale jeśli robisz to tylko wtedy, gdy jest to konieczne (czyli przechodząc z int do enum iirc), jest to całkowicie normalny kod, który ludzie widzieli wcześniej.

Mam też wątpliwości co do kosztu miejsca wyliczenia. Zmienne i parametry uint8 prawdopodobnie nie będą używać mniej stosu niż ints, więc liczy się tylko przechowywanie w klasach. Istnieją przypadki, w których pakowanie wielu bajtów w struct wygrywa (w takim przypadku można rzucać wyliczenia do i z pamięci uint8), ale zwykle wypełnienie i tak zabije korzyści.

Tak więc wyliczenie nie ma wad w porównaniu z innymi, a jako zaleta daje trochę bezpieczeństwa typu (nie można przypisać jakiejś losowej wartości całkowitej bez jawnego rzutowania) i czyste sposoby odwoływania się do wszystkiego.

Nawiasem mówiąc, wolałbym również umieścić „= 2” w wyliczeniu. Nie jest to konieczne, ale „zasada najmniejszego zdziwienia” sugeruje, że wszystkie 4 definicje powinny wyglądać tak samo.

Steve Jessop
źródło
1
Właściwie w ogóle nie brałem pod uwagę bitsetu. Jednak nie jestem pewien, czy byłoby dobrze. W przypadku zestawu bitów muszę adresować bity jako 1, 2, 3, 4, co sprawiłoby, że kod byłby mniej czytelny - co oznacza, że ​​prawdopodobnie użyłbym wyliczenia do „nazwania” bitów. Może to być jednak oszczędność miejsca. Dzięki.
Milan Babuškov
Milan, nie musisz „nazywać” bitów przy użyciu wyliczenia, możesz po prostu użyć predefiniowanych bitów, jak pokazano powyżej. Jeśli chcesz włączyć bit pierwszy zamiast my_bitset.flip (1), powinieneś zrobić my_bitset | = xNew;
moswald
jest to skierowane mniej do ciebie, a bardziej do STL, ale: naprawdę muszę zapytać: dlaczego miałbyś używać bitsettego? Zwykle przekłada się to na a long(w mojej implementacji iirc; tak, jak marnotrawne) lub podobny typ całkowy dla każdego elementu, więc dlaczego nie użyć po prostu całek nieobfuskowanych? (lub, obecnie, constexprbez pamięci)
underscore_d
[limit czasu edycji] ... ale tak naprawdę nigdy nie rozumiałem uzasadnienia dla tej bitsetklasy, poza tym, co wydaje się być powracającym wątkiem w otaczających dyskusjach: „ugh, musimy zatuszować niesmaczne korzenie języka na niskim poziomie” '
underscore_d
uint8zmienne i parametry prawdopodobnie nie będą używać mniej stosu niż ints” jest błędne. Jeśli masz procesor z 8-bitowymi rejestrami, intpotrzebujesz co najmniej 2 rejestrów, podczas gdy uint8_tpotrzebujesz tylko 1, więc będziesz potrzebować więcej miejsca na stosie, ponieważ istnieje większe prawdopodobieństwo, że zabraknie rejestrów (co jest również wolniejsze i może zwiększyć rozmiar kodu ( w zależności od zestawu instrukcji)). (Trzeba typu, powinno być uint8_tnie uint8)
12431234123412341234123
5

Jeśli to możliwe, NIE używaj makr. Nie są zbytnio podziwiani, jeśli chodzi o nowoczesny C ++.

INS
źródło
4
Prawdziwe. To, czego nienawidzę w makrach, to to, że nie możesz do nich wejść, jeśli są złe.
Carl
Wyobrażam sobie, że można to naprawić w kompilatorze.
celticminstrel
4

Wyliczenia byłyby bardziej odpowiednie, ponieważ zapewniają „znaczenie identyfikatorów”, a także bezpieczeństwo typów. Można wyraźnie stwierdzić, że „xDeleted” ma wartość „RecordType” i że reprezentuje „typ rekordu” (wow!) Nawet po latach. Const wymagałyby do tego komentarzy, a także wymagałyby poprawiania i zmniejszania kodu.

hayalci
źródło
4

Z definicjami tracę bezpieczeństwo typu

Niekoniecznie...

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

a przy wyliczeniu tracę trochę spacji (liczby całkowite)

Niekoniecznie - ale musisz być wyraźny w punktach przechowywania ...

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

i prawdopodobnie muszę rzutować, gdy chcę wykonać operację bitową.

Możesz stworzyć operatorów, aby wyeliminować ten ból:

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

Myślę, że w przypadku const tracę również bezpieczeństwo typu, ponieważ losowy uint8 może dostać się przez pomyłkę.

To samo może się zdarzyć z każdym z tych mechanizmów: sprawdzanie zakresu i wartości jest zwykle ortogonalne w stosunku do bezpieczeństwa typów (chociaż typy zdefiniowane przez użytkownika - tj. Własne klasy - mogą wymuszać „niezmienniki” dotyczące ich danych). W przypadku wyliczeń kompilator może wybrać większy typ do hostowania wartości, a niezainicjowana, uszkodzona lub po prostu źle ustawiona zmienna wyliczeniowa może nadal interpretować jej wzorzec bitowy jako liczbę, której nie można się spodziewać - porównując nierówną z dowolną identyfikatory wyliczenia, dowolna ich kombinacja oraz 0.

Czy jest jakiś inny czystszy sposób? / Jeśli nie, czego byś użył i dlaczego?

Cóż, w końcu wypróbowany i zaufany bitowy OR wyliczeń w stylu C działa całkiem nieźle, gdy na obrazie są pola bitowe i niestandardowe operatory. Możesz dalej poprawić swoją niezawodność za pomocą niektórych niestandardowych funkcji walidacji i asercji, jak w odpowiedzi mat_geek; techniki często równie stosowane do obsługi string, int, double wartości itp.

Można argumentować, że to jest „czystsze”:

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

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

Jestem obojętny: bity danych pakują się ciaśniej, ale kod znacznie się rozrasta ... zależy od liczby posiadanych obiektów, a lamdby - choć są piękne - są nadal bardziej niechlujne i trudniejsze do wykonania niż bitowe OR.

BTW / - argument o dość słabym IMHO bezpieczeństwa nici - najlepiej zapamiętany jako tło, a nie jako dominująca siła napędowa decyzji; udostępnianie muteksu na polach bitowych jest bardziej prawdopodobną praktyką, nawet jeśli nie zdajemy sobie sprawy z ich pakowania (muteksy są stosunkowo obszernymi członkami danych - muszę naprawdę martwić się o wydajność, aby rozważyć posiadanie wielu muteksów na elementach jednego obiektu i przyjrzałbym się uważnie wystarczy zauważyć, że były to pola bitowe). Każdy typ podrzędnego rozmiaru może mieć ten sam problem (np. A uint8_t). W każdym razie możesz wypróbować niepodzielne operacje w stylu porównania i zamiany, jeśli desperacko pragniesz większej współbieżności.

Tony Delroy
źródło
1
+1 Dobra. Ale przed instrukcją operator|należy rzutować na typ całkowity ( unsigned int) |. W przeciwnym razie operator|will rekurencyjnie wywoła siebie i spowoduje przepełnienie stosu w czasie wykonywania. Proponuję: return RecordType( unsigned(lhs) | unsigned(rhs) );. Pozdrawiam
olibre
3

Nawet jeśli musisz użyć 4 bajtów do przechowywania wyliczenia (nie jestem zaznajomiony z C ++ - wiem, że możesz określić typ bazowy w C #), nadal warto - użyj wyliczeń.

W dzisiejszych czasach serwerów z GB pamięci rzeczy takie jak 4 bajty w porównaniu z 1 bajtem pamięci na poziomie aplikacji w ogóle nie mają znaczenia. Oczywiście, jeśli w twojej konkretnej sytuacji użycie pamięci jest tak ważne (i nie możesz zmusić C ++ do używania bajtu do poparcia wyliczenia), możesz rozważyć trasę „static const”.

Pod koniec dnia musisz zadać sobie pytanie, czy warto zadać sobie pytanie, czy warto zastosować „statyczną stałą” dla 3 bajtów oszczędności pamięci dla struktury danych?

Coś innego, o czym należy pamiętać - IIRC, na x86, struktury danych są wyrównane 4-bajtowo, więc jeśli nie masz wielu elementów o szerokości bajtów w strukturze „rekordu”, może to nie mieć znaczenia. Przetestuj i upewnij się, że tak się stanie, zanim dokonasz kompromisu między konserwacją a wydajnością / przestrzenią.

Jonathan Rupp
źródło
Możesz określić typ bazowy w C ++, od wersji językowej C ++ 11. Do tego czasu uważam, że było to „co najmniej wystarczająco duże, aby przechowywać i być używane jako pole bitowe dla wszystkich określonych modułów wyliczających, ale prawdopodobnie intnie jest zbyt małe”. [Jeśli nie określisz typu bazowego w C ++ 11, używa on starszego zachowania. Odwrotnie, enum classtyp bazowy C ++ 11 jawnie przyjmuje wartość domyślną, intjeśli nie określono inaczej.]
Justin Time - Przywróć Monikę
3

Jeśli chcesz mieć bezpieczeństwo typów klas, z wygodą składni wyliczania i sprawdzaniem bitów, rozważ Bezpieczne etykiety w C ++ . Pracowałem z autorem i jest całkiem sprytny.

Uważaj jednak. Ostatecznie ten pakiet używa szablonów i makr!

Don Wakefield
źródło
Wygląda na przesadę dla mojej małej aplikacji. ale wydaje się, że to dobre rozwiązanie.
Milan Babuškov
2

Czy faktycznie musisz przekazywać wartości flag jako koncepcyjną całość, czy też będziesz mieć dużo kodu dla każdej flagi? Tak czy inaczej, myślę, że posiadanie tego jako klasy lub struktury 1-bitowych pól bitowych może być w rzeczywistości wyraźniejsze:

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

Wtedy twoja klasa rekordu mogłaby mieć zmienną składową typu struct RecordFlag, funkcje mogą przyjmować argumenty typu struct RecordFlag itp. Kompilator powinien spakować pola bitowe razem, oszczędzając miejsce.

mądry
źródło
Czasami jako całość, czasami jako flaga. Muszę też sprawdzić, czy jest ustawiona jakaś flaga (kiedy ją przejdę jako całość).
Milan Babuškov
cóż, gdy są oddzielne, po prostu poproś o int. Kiedy razem, przekaż strukturę.
mądry
Nie będzie lepiej. Dostęp do pól bitowych jest wolniejszy niż cokolwiek innego.
paercebal
Naprawdę? Myślisz, że kompilator wygeneruje znacząco inny kod do testowania pól bitowych niż ręczne manipulowanie bitami? I że będzie znacznie wolniej? Czemu? Jedyną rzeczą, której nie możesz zrobić tak łatwo idiomatycznie, jest maskowanie wielu flag jednocześnie.
mądry
Wykonując prosty test odczytu, otrzymuję 5,50-5,58 sekund dla maskowania bitów w porównaniu z 5,45-5,59 dla dostępu do pola bitowego. Prawie nie do odróżnienia.
mądry,
2

Prawdopodobnie nie użyłbym wyliczenia dla tego rodzaju rzeczy, w których wartości można łączyć ze sobą, częściej wyliczenia są wzajemnie wykluczającymi się stanami.

Jednak niezależnie od używanej metody, aby wyjaśnić, że są to wartości będące bitami, które można ze sobą łączyć, zamiast tego użyj następującej składni dla rzeczywistych wartości:

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

Przesunięcie w lewo pomaga wskazać, że każda wartość ma być pojedynczym bitem, jest mniej prawdopodobne, że później ktoś zrobi coś złego, na przykład doda nową wartość i przypisze jej wartość 9.


źródło
1
Jest do tego wystarczająco precedens, szczególnie w stałych dla ioctl (). Wolę jednak używać stałych szesnastkowych: 0x01, 0x02, 0x04, 0x08, 0x10, ...
Jonathan Leffler
2

Opierając się na KISS , wysokiej spójności i niskim sprzężeniu , zadaj następujące pytania -

  • Kto musi wiedzieć? moja klasa, moja biblioteka, inne klasy, inne biblioteki, strony trzecie
  • Jaki poziom abstrakcji muszę zapewnić? Czy konsument rozumie operacje bitowe.
  • Czy będę musiał interfejs z VB / C # itp.?

Jest świetna książka " Large-Scale C ++ Software Design ", która promuje typy podstawowe na zewnątrz, jeśli możesz uniknąć innych zależności pliku nagłówkowego / interfejsu, które powinieneś spróbować.

titanae
źródło
1
a) 5-6 lekcji. b) tylko ja, to jednoosobowy projekt c) brak połączenia
Milan Babuškov
2

Jeśli używasz Qt, powinieneś poszukać QFlags . Klasa QFlags zapewnia bezpieczny dla typu sposób przechowywania kombinacji LUB wartości wyliczenia.

Thomas Koschel
źródło
Nie, nie Qt. Właściwie to projekt wxWidgets.
Milan Babuškov
0

Wolałbym pójść z

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

Proste, ponieważ:

  1. Jest bardziej przejrzysty i sprawia, że ​​kod jest czytelny i łatwy w utrzymaniu.
  2. Logicznie grupuje stałe.
  3. Czas programisty jest ważniejszy, chyba że twoim zadaniem jest zaoszczędzenie tych 3 bajtów.
Vivek
źródło
Cóż, z łatwością mógłbym mieć milion wystąpień klasy Record, więc może to być ważne. OTOH, to tylko różnica między 1 MB a 4 MB, więc może nie powinienem się martwić.
Milan Babuškov
@Vivek: Czy rozważałeś ograniczenie szerokości całkowitej? W szczególności przed C ++ 11.
user2672165
0

Nie chodzi o to, że lubię przeprojektowywać wszystko, ale czasami w takich przypadkach może warto utworzyć (małą) klasę, aby hermetyzować te informacje. Jeśli utworzysz klasę RecordType, może ona mieć funkcje takie jak:

void setDeleted ();

void clearDeleted ();

bool isDeleted ();

etc ... (lub jakakolwiek inna konwencja)

Mógłby zweryfikować kombinacje (w przypadku, gdy nie wszystkie kombinacje są legalne, np. Jeśli nie można jednocześnie ustawić „nowego” i „usuniętego”). Jeśli właśnie użyłeś masek bitowych itp., Kod ustawiający stan musi zostać zweryfikowany, klasa może również hermetyzować tę logikę.

Klasa może również dać ci możliwość dołączania znaczących informacji logowania do każdego stanu, możesz dodać funkcję zwracającą ciąg reprezentujący bieżący stan itp. (Lub użyć operatorów przesyłania strumieniowego „<<”).

Mimo wszystko, jeśli martwisz się o pamięć, nadal możesz mieć klasę tylko z elementem danych „char”, więc zajmuj tylko niewielką ilość pamięci (zakładając, że nie jest wirtualna). Oczywiście w zależności od sprzętu itp. Mogą wystąpić problemy z wyrównaniem.

Rzeczywiste wartości bitowe mogą być niewidoczne dla reszty „świata”, jeśli znajdują się one w anonimowej przestrzeni nazw wewnątrz pliku cpp, a nie w pliku nagłówkowym.

Jeśli okaże się, że kod wykorzystujący enum / # define / bitmask etc ma dużo kodu „pomocniczego” do obsługi nieprawidłowych kombinacji, logowania itp., Wówczas warto rozważyć hermetyzację w klasie. Oczywiście w większości przypadków proste problemy są lepsze dzięki prostym rozwiązaniom ...

Benzoes
źródło
Niestety deklaracja musi znajdować się w pliku .h, ponieważ jest używana w całym projekcie (używana przez jakieś 5-6 klas).
Milan Babuškov