Czy wyliczenia C ++ są podpisane czy niepodpisane?

107

Czy wyliczenia C ++ są podpisane czy niepodpisane? A co za tym idzie, czy można bezpiecznie sprawdzić poprawność danych wejściowych, sprawdzając, czy jest to <= twoja maksymalna wartość, i pominąć> = twoją wartość minimalną (zakładając, że zacząłeś od 0 i zwiększałeś o 1)?

Matt
źródło
Kiedy używamy typu wyliczenia w kontekście, który wymaga jego znaku, w rzeczywistości mówimy o niejawnej konwersji wyliczenia na typ całkowity. Standard C ++ 03 mówi, że jest to wykonywane przez Integral Promotion, nic nie jest związane z podstawowym typem wyliczenia. Więc nie rozumiem, dlaczego każda odpowiedź tutaj wspomina, że ​​podstawowy typ nie jest zdefiniowany przez standard? Spodziewane zachowanie opisałem tutaj: stackoverflow.com/questions/24802322/ ...
JavaMan

Odpowiedzi:

60

Nie powinieneś polegać na żadnej konkretnej reprezentacji. Przeczytaj poniższy link . Ponadto standard mówi, że jest zdefiniowane w implementacji, który typ całkowity jest używany jako typ bazowy dla wyliczenia, z wyjątkiem tego, że nie powinien być większy niż int, chyba że pewna wartość nie może pasować do int lub unsigned int.

W skrócie: nie można polegać na tym, że wyliczenie jest podpisane lub niepodpisane.

zvrba
źródło
28
Odpowiedź Michaela Burra (która cytuje standard) w rzeczywistości sugeruje, że możesz polegać na podpisaniu, jeśli zdefiniujesz wartość wyliczenia jako ujemną, ponieważ typ może „reprezentować wszystkie wartości modułu wyliczającego zdefiniowane w wyliczeniu”.
Samuel Harmer,
101

Przejdźmy do źródła. Oto, co mówi dokument normy C ++ 03 (ISO / IEC 14882: 2003) w 7.2-5 (Deklaracje wyliczenia):

Podstawowy typ wyliczenia jest typem całkowitym, który może reprezentować wszystkie wartości modułu wyliczającego zdefiniowane w wyliczeniu. Jest zdefiniowany w implementacji, który typ całkowity jest używany jako typ bazowy dla wyliczenia, z tą różnicą, że typ bazowy nie powinien być większy niż int, chyba że wartość modułu wyliczającego nie mieści się w int lub unsigned int.

Krótko mówiąc, twój kompilator może dokonać wyboru (oczywiście, jeśli masz liczby ujemne dla niektórych wartości wyliczenia, zostanie to podpisane).

Michael Burr
źródło
Jak możemy uniknąć zgadywania kompilatora i powiedzieć mu, aby używał bazowego typu bez znaku, gdy wszystkie wartości wyliczenia są małymi dodatnimi liczbami całkowitymi? (Łapiemy wynik UBsan, ponieważ kompilator wybiera int i przepełnienie cierpienia int. Wartość jest bez znaku i dodatnia, a nasze użycie zależy od zawijania bez znaku, aby zapewnić dekrementację lub „krok ujemny”).
jww
@jww - to chyba będzie zależeć od używanego kompilatora. Ponieważ standard nie narzuca bazowego typu i pozostawia to implementacji, należy spojrzeć na dokumentację narzędzia i sprawdzić, czy ta opcja jest w ogóle możliwa. Jeśli chcesz zagwarantować określone zachowanie w kodzie, dlaczego nie rzutować elementu członkowskiego wyliczenia, którego używasz w wyrażeniu?
ysap
22

Nie powinieneś polegać na tym, że są podpisane lub niepodpisane. Jeśli chcesz, aby były jawnie podpisane lub niepodpisane, możesz użyć następujących opcji:

enum X : signed int { ... };    // signed enum
enum Y : unsigned int { ... };  // unsigned enum
Adam Rosenfield
źródło
11
Tylko w przyszłym standardzie C ++ 0x.
dalle
3
@dalle Microsoft compilator umożliwia również wpisywanie
wyliczeń
15

Nie powinieneś polegać na tym, że jest podpisany lub niepodpisany. Zgodnie ze standardem jest zdefiniowane w implementacji, który typ całkowity jest używany jako typ bazowy dla wyliczenia. Jednak w większości implementacji jest to liczba całkowita ze znakiem.

W C ++ 0x zostaną dodane silnie wpisane wyliczenia , które pozwolą określić typ wyliczenia, na przykład:

enum X : signed int { ... };    // signed enum
enum Y : unsigned int { ... };  // unsigned enum

Nawet teraz można jednak przeprowadzić prostą walidację, używając wyliczenia jako zmiennej lub typu parametru, takiego jak ten:

enum Fruit { Apple, Banana };

enum Fruit fruitVariable = Banana;  // Okay, Banana is a member of the Fruit enum
fruitVariable = 1;  // Error, 1 is not a member of enum Fruit
                    // even though it has the same value as banana.
Matt
źródło
Myślę, że twój drugi przykład jest trochę zagmatwany :)
Miral
5

Kompilator może zdecydować, czy wyliczenia są podpisane czy niepodpisane.

Inną metodą sprawdzania poprawności wyliczeń jest użycie samego wyliczenia jako typu zmiennej. Na przykład:

enum Fruit
{
    Apple = 0,
    Banana,
    Pineapple,
    Orange,
    Kumquat
};

enum Fruit fruitVariable = Banana;  // Okay, Banana is a member of the Fruit enum
fruitVariable = 1;  // Error, 1 is not a member of enum Fruit even though it has the same value as banana.
Cristián Romo
źródło
5

Nawet niektóre stare odpowiedzi otrzymały 44 głosy za, nie zgadzam się ze wszystkimi z nich. Krótko mówiąc, nie sądzę, że powinniśmy przejmować underlying typesię wyliczeniem.

Po pierwsze, typ Enum w C ++ 03 jest odrębnym typem, który nie ma pojęcia o znaku. Ponieważ od standardu C ++ 03dcl.enum

7.2 Enumeration declarations 
5 Each enumeration defines a type that is different from all other types....

Więc kiedy mówimy o znaku typu wyliczenia, powiedzmy, porównując 2 operandy wyliczenia przy użyciu <operatora, w rzeczywistości mówimy o niejawnej konwersji typu wyliczenia na typ całkowity. Liczy się znak tego integralnego typu . A podczas konwersji wyliczenia na typ całkowity ma zastosowanie ta instrukcja:

9 The value of an enumerator or an object of an enumeration type is converted to an integer by integral promotion (4.5).

I najwyraźniej podstawowy typ wyliczenia nie ma nic wspólnego z promocją integralną. Ponieważ standard definiuje integralną promocję w następujący sposób:

4.5 Integral promotions conv.prom
.. An rvalue of an enumeration type (7.2) can be converted to an rvalue of the first of the following types that can represent all the values of the enumeration
(i.e. the values in the range bmin to bmax as described in 7.2: int, unsigned int, long, or unsigned long.

W związku z tym, czy typ wyliczeniowy staje się signed intlub unsigned intzależy od tego, czy signed intmoże zawierać wszystkie wartości zdefiniowanych modułów wyliczających, a nie podstawowy typ wyliczenia.

Zobacz moje pokrewne pytanie Znak typu wyliczenia w C ++ nieprawidłowy po przekonwertowaniu na typ całkowy

JavaMan
źródło
Ma to znaczenie, gdy kompilujesz z -Wsign-conversion. Używamy go, aby pomóc wyłapać niezamierzone błędy w naszym kodzie. Ale +1 za cytowanie standardu i wskazanie, że wyliczenie nie ma żadnego typu (w signedporównaniu unsigned) z nim.
jww
4

W przyszłości w C ++ 0x będą dostępne wyliczenia o jednoznacznie określonym typie i będą miały kilka zalet (takich jak bezpieczeństwo typów, jawne typy bazowe lub jawne określanie zakresu). Dzięki temu możesz być pewniejszy co do oznaczenia typu.

Kris Kumler
źródło
4

Oprócz tego, co inni powiedzieli o podpisaniu / niepodpisaniu, oto, co mówi norma o zakresie wyliczanego typu:

7.2 (6): „W przypadku wyliczenia, w którym e (min) jest najmniejszym modułem wyliczającym, a e (max) jest największym, wartości wyliczenia są wartościami typu bazowego w zakresie od b (min) do b (max ), gdzie b (min) i b (max) to odpowiednio najmniejsze i największe wartości najmniejszego pola bitowego, które może przechowywać e (min) i e (max). Możliwe jest zdefiniowanie wyliczenia, które ma niezdefiniowane wartości przez któregokolwiek z jego wyliczających. "

Na przykład:

enum { A = 1, B = 4};

definiuje typ wyliczeniowy, gdzie e (min) to 1, a e (max) to 4. Jeśli typ bazowy jest podpisany int, to najmniejsze wymagane pole bitowe ma 4 bity, a jeśli ints w twojej implementacji są uzupełnieniami do dwóch, to prawidłowy zakres wyliczenie to od -8 do 7. Jeśli typ bazowy jest bez znaku, to ma 3 bity, a zakres wynosi od 0 do 7. Sprawdź dokumentację kompilatora, jeśli Ci zależy (na przykład jeśli chcesz rzutować wartości całkowite inne niż moduły wyliczające na typ wyliczeniowy, to musisz wiedzieć, czy wartość jest w zakresie wyliczenia, czy nie - jeśli nie, wynikowa wartość wyliczenia jest nieokreślona).

To, czy te wartości są prawidłowymi danymi wejściowymi dla funkcji, może być inną kwestią niż to, czy są one prawidłowymi wartościami typu wyliczeniowego. Twój kod sprawdzający prawdopodobnie martwi się raczej o to pierwsze niż o drugie, więc w tym przykładzie powinno przynajmniej sprawdzać> = A i <= B.

Steve Jessop
źródło
0

Sprawdź to z std::is_signed<std::underlying_typewyliczeniami + zakres domyślnieint

https://en.cppreference.com/w/cpp/language/enum oznacza:

main.cpp

#include <cassert>
#include <iostream>
#include <type_traits>

enum Unscoped {};
enum class ScopedDefault {};
enum class ScopedExplicit : long {};

int main() {
    // Implementation defined, let's find out.
    std::cout << std::is_signed<std::underlying_type<Unscoped>>() << std::endl;

    // Guaranteed. Scoped defaults to int.
    assert((std::is_same<std::underlying_type<ScopedDefault>::type, int>()));

    // Guaranteed. We set it ourselves.
    assert((std::is_same<std::underlying_type<ScopedExplicit>::type, long>()));
}

GitHub upstream .

Skompiluj i uruchom:

g++ -std=c++17 -Wall -Wextra -pedantic-errors -o main main.cpp
./main

Wynik:

0

Testowano na Ubuntu 16.04, GCC 6.4.0.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło