Co się stanie, jeśli static_cast niepoprawna wartość zostanie wyliczona w klasie?

146

Rozważmy ten kod w C ++ 11:

enum class Color : char { red = 0x1, yellow = 0x2 }
// ...
char *data = ReadFile();
Color color = static_cast<Color>(data[0]);

Załóżmy, że dane [0] to w rzeczywistości 100. Jaki jest kolor ustawiony zgodnie ze standardem? W szczególności, jeśli później to zrobię

switch (color) {
    // ... red and yellow cases omitted
    default:
        // handle error
        break;
}

czy standard gwarantuje, że zostanie osiągnięta wartość domyślna? Jeśli nie, jaki jest właściwy, najbardziej wydajny i elegancki sposób sprawdzenia tutaj błędu?

EDYTOWAĆ:

Jako bonus, czy standard zapewnia jakiekolwiek gwarancje, ale z prostym wyliczeniem?

darth happyface
źródło

Odpowiedzi:

131

Czym jest ustawiony kolor zgodnie ze standardem?

Odpowiadając cytatem ze standardów C ++ 11 i C ++ 14:

[expr.static.cast] / 10

Wartość typu całkowitego lub wyliczeniowego można jawnie przekonwertować na typ wyliczeniowy. Wartość pozostaje niezmieniona, jeśli oryginalna wartość mieści się w zakresie wartości wyliczenia (7.2). W przeciwnym razie wynikowa wartość jest nieokreślona (i może nie znajdować się w tym zakresie).

Spójrzmy na zakres wartości wyliczenia : [dcl.enum] / 7

W przypadku wyliczenia, którego typ bazowy jest ustalony, wartości wyliczenia są wartościami typu podstawowego.

Przed CWG 1766 (C ++ 11, C ++ 14) W związku z tym dla for data[0] == 100, wynikowa wartość jest określona (*) i nie jest zaangażowane żadne niezdefiniowane zachowanie (UB) . Mówiąc bardziej ogólnie, podczas rzutowania z typu podstawowego na typ wyliczeniowy żadna wartość w nie data[0]może prowadzić do UB dla static_cast.

Po CWG 1766 (C ++ 17) Zobacz defekt CWG 1766 . Przycisk [expr.static.cast] p10 ustęp została wzmocniona, więc teraz może powołać UB jeśli rzucisz wartość, która wykracza poza zakres reprezentowalna wyliczenia do typu enum. To nadal nie dotyczy scenariusza w pytaniu, ponieważ data[0]jest to podstawowy typ wyliczenia (patrz wyżej).

Należy pamiętać, że CWG 1766 jest uważane za wadę w standardzie, dlatego też jest akceptowane, aby implementatorzy kompilatora stosowali się do swoich trybów kompilacji C ++ 11 i C ++ 14.

(*) charmusi mieć co najmniej 8 bitów szerokości, ale nie musi unsigned. Wymagana jest maksymalna wartość, jaką można zapisać, co najmniej 127zgodnie z załącznikiem E normy C99.


Porównaj z [wyraż] / 4

Jeśli podczas oceny wyrażenia wynik nie jest matematycznie zdefiniowany lub nie mieści się w zakresie reprezentowalnych wartości dla jego typu, zachowanie jest niezdefiniowane.

Przed CWG 1766 typ całkowity konwersji -> typ wyliczeniowy może generować nieokreśloną wartość . Pytanie brzmi: czy nieokreślona wartość może znajdować się poza reprezentowalnymi wartościami dla swojego typu? Uważam, że odpowiedź brzmi nie - jeśli odpowiedź byłaby twierdząca , nie byłoby żadnej różnicy w gwarancjach, jakie otrzymujesz dla operacji na typach ze znakiem między „ta operacja daje nieokreśloną wartość” i „ta operacja ma nieokreślone zachowanie”.

Stąd przed działań grupy roboczej 1766, nawet static_cast<Color>(10000)by nie invoke UB; ale po działań grupy roboczej 1766, to robi invoke UB.


Teraz switchoświadczenie:

[stmt.switch] / 2

Warunek powinien być typu całkowitego, typu wyliczenia lub typu klasy. [...] Prowadzone są promocje integralne.

[konw.prom] / 4

Wartość prvalue typu wyliczenia bez zakresu, którego typ bazowy jest ustalony (7.2), można przekonwertować na wartość prvalue jego typu bazowego. Ponadto, jeśli integralną promocję można zastosować do jej typu podstawowego, prvalue typu wyliczenia bez zakresu, którego typ bazowy jest ustalony, można również przekonwertować na prvalue promowanego typu podstawowego.

Uwaga: bazowy typ wyliczenia o określonym zakresie bez podstawy wyliczenia to int. W przypadku wyliczeń bez zakresu typ podstawowy jest zdefiniowany w ramach implementacji, ale nie powinien być większy niż intjeśli intmoże zawierać wartości wszystkich modułów wyliczających.

W przypadku wyliczenia bez zakresu prowadzi to do / 1

Prvalue od typu całkowitych innych niż bool, char16_t, char32_tlub wchar_tktórej całkowita konwersja stopień (4,13) jest mniejszy niż stopień intmoże być przekształcany do prvalue typu intczy intmoże reprezentować wszystkie wartości typu źródłowego; w przeciwnym razie źródłowa prvalue może zostać przekonwertowana na prvalue typu unsigned int.

W przypadku unscoped wyliczenie, będziemy mieć do czynienia ze ints tutaj. W przypadku wyliczeń w określonym zakresie ( enum classi enum struct) nie ma zastosowania żadna promocja integralna. W żaden sposób integralna promocja również nie prowadzi do UB, ponieważ przechowywana wartość znajduje się w zakresie typu bazowego iw zakresie int.

[stmt.switch] / 5

Kiedy switchinstrukcja jest wykonywana, jej stan jest oceniany i porównywany z każdą stałą przypadku. Jeśli jedna ze stałych wielkości przypadku jest równa wartości warunku, sterowanie jest przekazywane do instrukcji następującej po dopasowanej caseetykiecie. Jeśli żadna casestała nie pasuje do warunku i jeśli istnieje defaultetykieta, sterowanie przechodzi do instrukcji oznaczonej defaultetykietą.

defaultEtykieta powinna być hit.

Uwaga: można spojrzeć jeszcze raz na operator porównania, ale nie jest on wyraźnie używany w opisywanym „porównaniu”. W rzeczywistości nie ma żadnej wskazówki, że w naszym przypadku wprowadziłoby to UB dla wyliczeń z zakresem lub bez zakresu.


Jako bonus, czy standard zapewnia jakiekolwiek gwarancje, ale z prostym wyliczeniem?

To, czy enumjest to zakres, nie ma tutaj znaczenia. Jednak ma znaczenie, czy typ bazowy jest ustalony, czy nie. Kompletne [decl.enum] / 7 to:

W przypadku wyliczenia, którego typ bazowy jest ustalony, wartości wyliczenia są wartościami typu podstawowego. W przeciwnym razie, dla wyliczenia, w którym e min jest najmniejszym wyliczaczem, a e max jest największym, wartościami wyliczenia są wartości z zakresu od b min do b max , zdefiniowane w następujący sposób: Niech Kbędzie 1dla reprezentacji dopełnienia do dwóch i 0dla a czyjś dopełnienie lub reprezentacja wielkości znaku. b max jest najmniejszą wartością większą lub równą max (| e min | - K, | e max |) i równą 2M - 1 , gdzieMjest nieujemną liczbą całkowitą. b min wynosi zero, jeśli e min jest nieujemne i - (b max + K) w przeciwnym razie.

Rzućmy okiem na następujące wyliczenie:

enum ColorUnfixed /* no fixed underlying type */
{
    red = 0x1,
    yellow = 0x2
}

Należy zauważyć, że nie możemy tego zdefiniować jako wyliczenia o określonym zakresie, ponieważ wszystkie wyliczenia w zakresie mają ustalone typy podstawowe.

Na szczęście ColorUnfixednajmniejszy moduł wyliczający to red = 0x1, więc max (| e min | - K, | e max |) jest równe | e max | w każdym razie, czyli yellow = 0x2. Najmniejsza wartość większa lub równa 2, która jest równa 2 M - 1 dla dodatniej liczby całkowitej Mto 3( 2 2 - 1 ). (Myślę, że celem jest zezwolenie na rozszerzenie zakresu w krokach 1-bitowych.) Wynika z tego, że b max jest, 3a bmin jest 0.

W związku z tym 100byłoby poza zakresem ColorUnfixedi static_castspowodowałoby nieokreśloną wartość przed CWG 1766 i niezdefiniowane zachowanie po CWG 1766.

dyp
źródło
3
Typ bazowy jest ustalony, więc zakres wartości wyliczenia (§7.2 [dcl.enum] p7) to „wartości typu podstawowego”. 100 to z pewnością wartość char, więc „Wartość pozostaje niezmieniona, jeśli oryginalna wartość mieści się w zakresie wartości wyliczenia (7.2)”. dotyczy.
Casey,
2
Musiałem poszukać, co oznacza „UB”. („niezdefiniowane zachowanie”) Pytanie nie wspominało o możliwości nieokreślonego zachowania; więc nie przyszło mi do głowy, że możesz o tym mówić.
karadoc
2
@karadoc Dodałem link przy pierwszym wystąpieniu terminu.
dyp
1
Uwielbiam tę odpowiedź. Dla tych, którzy przeglądają zbyt szybko, zauważ, że ostatnie zdanie „Dlatego 100 byłoby poza zakresem…” ma zastosowanie tylko wtedy, gdy kod został zmodyfikowany w celu usunięcia podstawowej specyfikacji typu (w tym przypadku char). W każdym razie myślę, że o to mi chodziło.
Eric Seppanen
1
@Ruslan CWG 1766 (lub jego rozwiązanie) nie jest częścią C ++ 14, ale myślę, że będzie częścią C ++ 17. Nawet przy regułach C ++ 17 nie do końca rozumiem, co masz na myśli mówiąc „unieważnij dalszy tekst odpowiedzi”. Inne części mojej odpowiedzi dotyczą głównie tego, że „zakres wartości wyliczenia” jest tym, do którego odnosi się expr.static.cast p10.
dyp