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 enum
tracę trochę spacji (liczby całkowite) i prawdopodobnie muszę rzucać, gdy chcę wykonać operację bitową. Z const
Myślę też stracić typu bezpieczeństwa, ponieważ losowa uint8
moż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 #define
s, a użyłem przestrzeni nazw i szablonów w kilku miejscach, więc to też nie jest wykluczone.
źródło
enum RecordType : uint8_t
łączy w sobie bezpieczeństwo typówenum
z niewielkim rozmiaremuint8_t
, chociaż nadal będziesz musiał zapewnić operatory bitowe.Odpowiedzi:
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.
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 ++.
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.
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ż
typedef
jest to po prostu anint
i wartość, taka jak0xDEADBEEF
może znajdować się w zmiennej za pośrednictwem niezainicjowanych lub nieprawidłowo ustawionych zmiennych.Dodaj
using
dyrektywę, jeśli chcesz często używać tego typu.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ć.
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.
źródło
IsValidMask
nie można było wybrać żadnego (tj.0
)?Zapomnij o definicjach
Zanieczyszczą twój kod.
bitfields?
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.
Ź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?
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:
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.
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:
Jak widzisz, Twoje wyliczenie zanieczyszcza globalną przestrzeń nazw. Jeśli umieścisz to wyliczenie w przestrzeni nazw, będziesz mieć coś takiego:
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:
I:
Nie będziesz jednak mógł użyć przełącznika na tych stałych. Więc w końcu wybierz swoją truciznę ... :-p
źródło
Czy wykluczyłeś std :: bitset? Do tego służą zestawy flag. Robić
następnie
Ponieważ istnieje wiele przeciążeń operatorów dla zestawu bitów, możesz teraz to zrobić
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.
źródło
bitset
tego? Zwykle przekłada się to na along
(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,constexpr
bez pamięci)bitset
klasy, poza tym, co wydaje się być powracającym wątkiem w otaczających dyskusjach: „ugh, musimy zatuszować niesmaczne korzenie języka na niskim poziomie” 'uint8
zmienne i parametry prawdopodobnie nie będą używać mniej stosu niżints
” jest błędne. Jeśli masz procesor z 8-bitowymi rejestrami,int
potrzebujesz co najmniej 2 rejestrów, podczas gdyuint8_t
potrzebujesz 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_t
nieuint8
)Oto kilka artykułów o stałych, makrach i wyliczeniach:
Stałe symboliczne Stałe
wyliczeniowe a obiekty stałe
Myślę, że powinieneś unikać makr, zwłaszcza że większość twojego nowego kodu napisałeś w nowoczesnym C ++.
źródło
Jeśli to możliwe, NIE używaj makr. Nie są zbytnio podziwiani, jeśli chodzi o nowoczesny C ++.
źródło
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.
źródło
Niekoniecznie...
Niekoniecznie - ale musisz być wyraźny w punktach przechowywania ...
Możesz stworzyć operatorów, aby wyeliminować ten ból:
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.
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”:
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.źródło
operator|
należy rzutować na typ całkowity (unsigned int
)|
. W przeciwnym razieoperator|
will rekurencyjnie wywoła siebie i spowoduje przepełnienie stosu w czasie wykonywania. Proponuję:return RecordType( unsigned(lhs) | unsigned(rhs) );
. PozdrawiamNawet 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ą.
źródło
int
nie jest zbyt małe”. [Jeśli nie określisz typu bazowego w C ++ 11, używa on starszego zachowania. Odwrotnie,enum class
typ bazowy C ++ 11 jawnie przyjmuje wartość domyślną,int
jeśli nie określono inaczej.]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!
źródło
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:
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.
źródło
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:
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
Opierając się na KISS , wysokiej spójności i niskim sprzężeniu , zadaj następujące pytania -
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ć.
źródło
Jeśli używasz Qt, powinieneś poszukać QFlags . Klasa QFlags zapewnia bezpieczny dla typu sposób przechowywania kombinacji LUB wartości wyliczenia.
źródło
Wolałbym pójść z
Proste, ponieważ:
źródło
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 ...
źródło