Rozważ następującą instrukcję wyliczania i przełączania:
typedef enum {
MaskValueUno,
MaskValueDos
} testingMask;
void myFunction(testingMask theMask) {
switch (theMask) {
case MaskValueUno: {}// deal with it
case MaskValueDos: {}// deal with it
default: {} //deal with an unexpected or uninitialized value
}
};
Jestem programistą Objective-C, ale napisałem to w czystym C dla szerszej publiczności.
Clang / LLVM 4.1 z -Wszystko ostrzega mnie w domyślnej linii:
Domyślna etykieta w przełączniku, która obejmuje wszystkie wartości wyliczeń
Teraz rozumiem, dlaczego tak jest: w idealnym świecie jedynymi wartościami wprowadzanymi do argumentu theMask
byłyby wyliczenie, więc żadne domyślne nie jest konieczne. Ale co jeśli jakiś hack pojawi się i wrzuci niezainicjowaną int do mojej pięknej funkcji? Moja funkcja zostanie udostępniona jako zrzut w bibliotece i nie mam kontroli nad tym, co może się tam znaleźć. Używanie default
jest bardzo zgrabnym sposobem radzenia sobie z tym.
Dlaczego bogowie LLVM uważają to zachowanie za niegodne ich piekielnego urządzenia? Czy powinienem poprzedzać to instrukcją if, aby sprawdzić argument?
źródło
"Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode."
.-Weverything
może być przydatny, ale należy uważać na zbyt częste mutowanie kodu, aby sobie z tym poradzić. Niektóre z tych ostrzeżeń są nie tylko bezwartościowe, ale przynoszą efekt przeciwny do zamierzonego i najlepiej je wyłączyć. (Rzeczywiście, taki jest przypadek użycia-Weverything
: zacznij od włączenia i wyłącz to, co nie ma sensu.)Odpowiedzi:
Oto wersja, która nie cierpi ani z powodu zgłaszania problemów, ani tej, przed którą się strzeżesz:
Killian wyjaśnił już, dlaczego brzęk emituje ostrzeżenie: jeśli rozszerzysz wyliczenie, wpadniesz w domyślny przypadek, który prawdopodobnie nie jest tym, czego chcesz. Właściwe jest usunięcie domyślnej skrzynki i otrzymanie ostrzeżeń o nieobsługiwanych warunkach.
Teraz obawiasz się, że ktoś może wywołać twoją funkcję z wartością spoza wyliczenia. To brzmi jak niespełnienie warunku wstępnego funkcji: udokumentowano, że oczekuje wartości z
testingMask
wyliczenia, ale programista przeszedł coś innego. Zrób więc błąd programisty przy użyciuassert()
(lubNSCAssert()
jak powiedziałeś, że używasz Objective-C). Spraw, aby Twój program się zawiesił, wyświetlając komunikat wyjaśniający, że programista robi to źle, jeśli programista robi to źle.źródło
return;
i dodać znakassert(false);
na końcu (zamiast powtarzać się, wymieniając prawne wyliczenia na początkuassert()
i na końcuswitch
).Posiadanie
default
tutaj etykiety jest wskaźnikiem, że nie rozumiesz, czego się spodziewasz. Ponieważ wszystkieenum
wartości zostały jawnie wyczerpane ,default
prawdopodobnie nie można ich wykonać i nie trzeba ich też chronić przed przyszłymi zmianami, ponieważ jeśli rozszerzyszenum
, to konstrukcja wygeneruje już ostrzeżenie.Tak więc kompilator zauważa, że obejrzałeś wszystkie podstawy, ale wydaje się, że myślisz , że nie, i to zawsze jest zły znak. Podejmując minimalne wysiłki, aby zmienić
switch
oczekiwaną formę, demonstrujesz kompilatorowi, że to, co wydaje się robić, jest tym, co faktycznie robisz, i wiesz o tym.źródło
Clang jest zdezorientowany, mając domyślne stwierdzenie, że istnieje doskonała praktyka, jest znany jako programowanie defensywne i jest uważany za dobrą praktykę programowania (1). Jest często używany w systemach o znaczeniu krytycznym, choć może nie w programowaniu na pulpicie.
Programowanie obronne ma na celu wychwycenie nieoczekiwanych błędów, które teoretycznie nigdy by się nie zdarzyły. Taki nieoczekiwany błąd niekoniecznie oznacza, że programista podaje funkcji niepoprawne dane wejściowe, a nawet „zły hack”. Bardziej prawdopodobne jest, że może to być spowodowane uszkodzoną zmienną: przyczyną mogą być przepełnienia bufora, przepełnienie stosu, niekontrolowany kod i podobne błędy niezwiązane z funkcją. W przypadku systemów wbudowanych zmienne mogą się zmieniać z powodu zakłóceń elektromagnetycznych, szczególnie jeśli używasz zewnętrznych obwodów pamięci RAM.
Co do tego, co napiszę w domyślnej instrukcji ... jeśli podejrzewasz, że program zwariował, gdy tam trafiłeś, potrzebujesz jakiejś obsługi błędów. W wielu przypadkach prawdopodobnie możesz po prostu dodać puste zdanie z komentarzem: „nieoczekiwany, ale nie ma znaczenia” itp., Aby pokazać, że zastanowiłeś się nad nieoczekiwaną sytuacją.
(1) MISRA-C: 2004 15.3.
źródło
-Weverything
włącza każde ostrzeżenie, a nie wyborem odpowiednim dla konkretnego przypadku użycia. Nie pasowanie do gustu nie jest tym samym, co zamieszanie.Jeszcze lepiej:
Jest to mniej podatne na błędy podczas dodawania elementów do wyliczenia. Możesz pominąć test dla> = 0, jeśli twoje wartości wyliczone są niepodpisane. Ta metoda działa tylko wtedy, gdy nie ma luk w wartościach wyliczeniowych, ale często tak jest.
źródło
Wtedy otrzymasz niezdefiniowane zachowanie i swoje
default
wola będzie bez znaczenia. Nic nie możesz zrobić, aby to poprawić.Pozwól mi być bardziej jasne. Gdy tylko ktoś przejdzie
int
w twoją funkcję niezainicjalizowany , stanie się niezdefiniowanym. Twoja funkcja może rozwiązać problem zatrzymania i nie ma znaczenia. To jest UB. Po wywołaniu UB nie można nic zrobić.źródło
Domyślna instrukcja niekoniecznie pomoże. Jeśli przełącznik znajduje się nad wyliczeniem, każda wartość, która nie jest zdefiniowana w wyliczeniu, spowoduje niezdefiniowane zachowanie.
Z tego co wiesz, kompilator może skompilować ten przełącznik (z ustawieniem domyślnym) jako:
Po uruchomieniu nieokreślonego zachowania nie ma już powrotu.
źródło
enum
typu jest taki sam, jak zakres wartości leżącego u jego podstaw typu liczb całkowitych (który jest zdefiniowany w implementacji, dlatego musi być spójny).Ja też wolę mieć
default:
we wszystkich przypadkach. Jak zwykle spóźniam się na przyjęcie, ale ... kilka innych myśli, których nie widziałem powyżej:-Werror
) pochodzi od-Wcovered-switch-default
(z,-Weverything
ale nie-Wall
). Jeśli twoja moralna elastyczność pozwala ci wyłączyć niektóre ostrzeżenia (tj. Wyciąć niektóre rzeczy z-Wall
lub-Weverything
), rozważ rzucanie-Wno-covered-switch-default
(lub-Wno-error=covered-switch-default
podczas używania-Werror
), i ogólnie-Wno-...
dla innych ostrzeżeń, które uważasz za nieprzyjemne.gcc
(i bardziej ogólne zachowanie wclang
), skonsultowaćgcc
manpage dla-Wswitch
,-Wswitch-enum
,-Wswitch-default
dla (różnych) zachowanie w podobnych sytuacjach wymienionych typów w sprawozdaniu przełącznika.Nie podoba mi się też to ostrzeżenie w koncepcji i nie podoba mi się jego brzmienie; dla mnie słowa z ostrzeżenia („etykieta domyślna ... obejmuje wszystkie ... wartości”) sugerują, że
default:
sprawa będzie zawsze wykonywana, na przykładPrzy pierwszym czytaniu myślałem, że na to wpadłeś - że twoja
default:
sprawa będzie zawsze wykonywana, ponieważ nie mabreak;
lub nie mareturn;
podobnego. Ta koncepcja jest podobna (do mojego ucha) do innych komentarzy w stylu niani (choć czasami pomocnych), z których wyłania sięclang
. Jeślifoo == 1
obie funkcje zostaną wykonane; powyższy kod ma takie zachowanie. Tj. Nie przełamuj się tylko wtedy, gdy chcesz nadal wykonywać kod z kolejnych przypadków! Wydaje się to jednak nie być twoim problemem.Ryzykując pedantyczne, kilka innych myśli o kompletności:
int
lub coś do tej funkcji, która jest wyraźnie zamierzający konsumować własny typ specyficzny, kompilator powinien zabezpieczyć się równie dobrze w tej sytuacji z agresywnym ostrzeżenia lub błędu. Ale tak nie jest! (To znaczy, wydaje się, że co najmniejgcc
iclang
nie róbenum
typu sprawdzania, ale słyszę, żeicc
robi ). Ponieważ nie otrzymujesz bezpieczeństwa typu , możesz uzyskać bezpieczeństwo wartości, jak omówiono powyżej. W przeciwnym razie, jak zasugerowano w TFA, zastanów się nadstruct
czymś, co może zapewnić bezpieczeństwo typu.enum
przykładMaskValueIllegal
, i nie wspieranie tegocase
w twoimswitch
. Zostałby zjedzony przezdefault:
(oprócz każdej innej zwariowanej wartości)Niech żyje kodowanie obronne!
źródło
Oto alternatywna sugestia:
OP stara się chronić przed przypadkiem, gdy ktoś przechodzi w miejscu, w
int
którym spodziewany jest wyliczenie . Lub, bardziej prawdopodobne, gdy ktoś połączył starą bibliotekę z nowszym programem przy użyciu nowszego nagłówka z większą liczbą przypadków.Dlaczego nie zmienić przełącznika do obsługi
int
skrzynki? Dodanie rzutowania przed wartością w przełączniku eliminuje ostrzeżenie, a nawet daje pewien podpowiedź, dlaczego istnieje wartość domyślna.Uważam, że jest to o wiele mniej budzące zastrzeżenia niż
assert()
testowanie każdej z możliwych wartości, a nawet przyjmowanie założenia, że zakres wartości wyliczeniowych jest dobrze uporządkowany, dzięki czemu działa prostszy test. Jest to po prostu brzydki sposób robienia tego, co domyślne robi dokładnie i pięknie.źródło