enum X : int
(C #) lub enum class X : int
(C ++ 11) jest typem, który ma ukryte wewnętrzne pole int
, które może pomieścić dowolną wartość. Ponadto X
w wyliczeniu zdefiniowano szereg predefiniowanych stałych . Możliwe jest rzutowanie wyliczenia na jego liczbę całkowitą i odwrotnie. Dotyczy to zarówno C #, jak i C ++ 11.
W języku C # wyliczenia są używane nie tylko do przechowywania pojedynczych wartości, ale także do przechowywania bitowych kombinacji flag, zgodnie z zaleceniami Microsoftu . Takie wyliczenia są (zwykle, ale niekoniecznie) ozdobione [Flags]
atrybutem. Aby ułatwić życie programistom, operatory bitowe (LUB, AND itd.) Są przeciążone, dzięki czemu można łatwo zrobić coś takiego (C #):
void M(NumericType flags);
M(NumericType.Sign | NumericType.ZeroPadding);
Jestem doświadczonym programistą C #, ale programuję C ++ dopiero od kilku dni i nie znam konwencji C ++. Zamierzam używać wyliczenia C ++ 11 dokładnie w taki sam sposób, jak to robiłem w C #. W C ++ 11 operatory bitowe w wyliczeniach zakresowych nie są przeciążone, więc chciałem je przeciążyć .
To wywołało debatę, a opinie wydają się różnić między trzema opcjami:
Zmienna typu wyliczeniowego służy do przechowywania pola bitowego, podobnie do C #:
void M(NumericType flags); // With operator overloading: M(NumericType::Sign | NumericType::ZeroPadding); // Without operator overloading: M(static_cast<NumericType>(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding)));
Byłoby to jednak sprzeczne z silnie wpisaną filozofią wyliczania enum w C ++ 11.
Użyj zwykłej liczby całkowitej, jeśli chcesz przechowywać bitową kombinację wyliczeń:
void M(int flags); M(static_cast<int>(NumericType::Sign) | static_cast<int>(NumericType::ZeroPadding));
Ale to zredukowałoby wszystko do
int
, pozostawiając cię bez pojęcia, jaki typ powinieneś zastosować w metodzie.Napisz osobną klasę, która przeciąża operatorów i przechowuje flagi bitowe w ukrytym polu liczby całkowitej:
class NumericTypeFlags { unsigned flags_; public: NumericTypeFlags () : flags_(0) {} NumericTypeFlags (NumericType t) : flags_(static_cast<unsigned>(t)) {} //...define BITWISE test/set operations }; void M(NumericTypeFlags flags); M(NumericType::Sign | NumericType::ZeroPadding);
( pełny kod autorstwa użytkownika315052 )
Ale wtedy nie masz IntelliSense ani żadnego wsparcia, które wskazywałoby na możliwe wartości.
Wiem, że to subiektywne pytanie , ale: Jakiego podejścia powinienem użyć? Jakie podejście jest najbardziej rozpowszechnione w C ++? Jakiego podejścia używasz w przypadku pól bitowych i dlaczego ?
Oczywiście, ponieważ wszystkie trzy podejścia działają, szukam przyczyn faktycznych i technicznych, ogólnie przyjętych konwencji, a nie tylko osobistych preferencji.
Na przykład, z powodu mojego tła w języku C #, zwykle używam podejścia 1 w C ++. Ma to tę dodatkową zaletę, że moje środowisko programistyczne może podpowiedzieć mi o możliwych wartościach, a przy przeciążonych operatorach enum jest to łatwe do napisania i zrozumienia oraz całkiem czyste. Podpis metody pokazuje wyraźnie, jakiej wartości oczekuje. Ale większość ludzi tutaj się ze mną nie zgadza, prawdopodobnie z ważnego powodu.
źródło
enum E { A = 1, B = 2, C = 4, };
zakresu jest0..7
(3 bity). W związku z tym standard C ++ wyraźnie gwarantuje, że nr 1 zawsze będzie realną opcją. [W szczególnościenum class
domyślnie,enum class : int
chyba że określono inaczej, a zatem zawsze ma ustalony typ bazowy.])Odpowiedzi:
Najprostszym sposobem jest zapewnienie sobie przeciążenia operatora. Zastanawiam się nad utworzeniem makra w celu rozszerzenia podstawowych przeciążeń według typu.
(Uwaga:
type_traits
jest to nagłówek C ++ 11 istd::underlying_type_t
funkcja C ++ 14).źródło
static_cast<T>
do wprowadzania danych, a rzutowanie w stylu C dla wyników tutaj?SBJFrameDrag
jest zdefiniowany w klasie, a|
-operator jest później używany w definicjach tej samej klasy, to jak zdefiniowałbyś operatora, aby mógł być użyty w klasie?Historycznie zawsze używałbym starego (słabo wpisanego) wyliczenia do nazwania stałych bitowych, a po prostu jawnie używałbym klasy pamięci do przechowywania wynikowej flagi. Tutaj spoczywałby na mnie obowiązek upewnienia się, że moje wyliczenia pasują do typu pamięci i śledzenia powiązania między polem a powiązanymi stałymi.
Podoba mi się pomysł silnie typowanych wyliczeń, ale nie bardzo podoba mi się pomysł, że zmienne typu wyliczeniowego mogą zawierać wartości, które nie należą do stałych tego wyliczenia.
Np. Zakładając bitowe lub zostało przeciążone:
W przypadku trzeciej opcji potrzebujesz płyty kotłowej, aby wyodrębnić typ przechowywania wyliczenia. Zakładając, że chcemy wymusić niepodpisany typ bazowy (możemy również obsłużyć podpisany, z odrobiną kodu):
To wciąż nie zapewnia IntelliSense ani autouzupełniania, ale wykrywanie typu pamięci jest mniej brzydkie, niż początkowo się spodziewałem.
Teraz znalazłem alternatywę: możesz określić typ magazynu dla słabo wpisanego wyliczenia. Ma nawet taką samą składnię jak w C #
Ponieważ jest słabo wpisany i niejawnie konwertuje do / z int (lub dowolnego innego rodzaju pamięci), mniej dziwnie jest mieć wartości, które nie pasują do wyliczonych stałych.
Minusem jest to, że jest to określane jako „przejściowe” ...
NB. ten wariant dodaje swoje wyliczone stałe zarówno do zagnieżdżonego, jak i obejmującego zakresu, ale można obejść to za pomocą przestrzeni nazw:
źródło
Możesz zdefiniować bezpieczne enum flagi w C ++ 11 za pomocą
std::enable_if
. Jest to podstawowa implementacja, w której może brakować niektórych rzeczy:Zauważ, żenumber_of_bits
kompilator nie może go wypełnić, ponieważ C ++ nie ma możliwości introspekcji możliwych wartości wyliczenia.Edycja: Właściwie to jestem poprawiony, można uzyskać
number_of_bits
dla ciebie kompilator .Zauważ, że to może obsłużyć (bardzo nieefektywnie) nieciągły zakres wartości wyliczenia. Powiedzmy, że nie jest dobrym pomysłem stosowanie powyższego z wyliczeniem takim jak ten, w przeciwnym razie nastąpi szaleństwo:
Ale wszystko, co uważa się za rozwiązanie, jest w końcu całkiem użytecznym rozwiązaniem. Nie wymaga żadnego kręcenia bitów po stronie użytkownika, jest bezpieczny dla typu i w swoich granicach, tak wydajny, jak to tylko możliwe (mocno opieram się na
std::bitset
jakości implementacji;)
).źródło
ja
nienawidzićnie cierpię makr w moim C ++ 14 tak bardzo, jak następnego faceta, ale zabrałem się do tego wszędzie i całkiem swobodnie:Użycie tak proste jak
I, jak mówią, dowodem jest pudding:
Zapraszam do niezdefiniowania poszczególnych operatorów według własnego uznania, ale moim bardzo stronniczym zdaniem C / C ++ służy do łączenia się z niskopoziomowymi koncepcjami i strumieniami, i możesz wyciągać te bitowe operatory z moich zimnych, martwych rąk i będę walczył z wszystkimi bezbożnymi makrami i zaklęciami, które potrafię wyczarować, aby je zatrzymać.
źródło
std::enable_if
z,std::is_enum
aby ograniczyć przeciążenia operatora do pracy tylko z wyliczonymi typami. Dodałem także operatory porównania (za pomocąstd::underlying_type
) i operator logiczny not, aby dodatkowo wypełnić lukę bez utraty mocnego pisania. Jedyne co mi się nie może dopasować jest niejawna konwersja do bool, aleflags != 0
i!flags
są wystarczające dla mnie.Zwykle należy zdefiniować zestaw wartości całkowitych odpowiadających liczbom binarnym ustawionym na jeden bit, a następnie dodać je razem. Tak zwykle robią to programiści C.
Więc miałbyś (używając operatora bitshift do ustawiania wartości, np. 1 << 2 jest taki sam jak binarny 100)
itp
W C ++ masz więcej opcji, zdefiniuj nowy typ raczej int (użyj typedef ) i podobnie ustaw wartości jak wyżej; lub zdefiniuj pole bitowe lub wektor booli . Ostatnie 2 są bardzo wydajne przestrzennie i mają znacznie większy sens w radzeniu sobie z flagami. Pole bitowe ma tę zaletę, że daje ci sprawdzanie typu (a zatem inteligencję).
Powiedziałbym (oczywiście subiektywnie), że programista C ++ powinien użyć pola bitowego dla twojego problemu, ale zwykle widzę podejście #define używane przez programy C często w programach C ++.
Przypuszczam, że pole bitowe jest najbliższe wyliczeniu C #, dlaczego C # próbował przeciążać wyliczenie, aby był typem pola bitowego, jest dziwne - wyliczenie powinno naprawdę być typem pojedynczego wyboru.
źródło
0b0100
), Więc1 << n
format jest w pewnym sensie przestarzały.Krótki przykład flag enum poniżej wygląda prawie jak C #.
O podejściu, moim zdaniem: mniej kodu, mniej błędów, lepszy kod.
ENUM_FLAGS (T) jest makrem zdefiniowanym w enum_flags.h (mniej niż 100 linii, można go używać bez ograniczeń).
źródło
::type
tam. Poprawiono: paste.ubuntu.com/23884820Istnieje jeszcze inny sposób na skórowanie kota:
Zamiast przeciążać operatory bitowe, przynajmniej niektórzy wolą po prostu dodać 4 linijkę, aby pomóc ci obejść to paskudne ograniczenie wyliczeń zakresowych:
To prawda, że musisz za
ut_cast()
każdym razem wpisywać tę rzecz, ale z drugiej strony daje to bardziej czytelny kod, w tym samym sensie, co użyciestatic_cast<>()
, w porównaniu z niejawną konwersją typu lub czymś innymoperator uint16_t()
.I bądźmy szczerzy tutaj, użycie typu
Foo
jak w powyższym kodzie ma swoje niebezpieczeństwa:Gdzieś indziej ktoś mógłby zmienić przypadek na zmienną
foo
i nie oczekiwać, że ma więcej niż jedną wartość ...Zaśmiecanie kodu
ut_cast()
pomaga ostrzegać czytelników, że dzieje się coś podejrzanego.źródło