Niektóre odpowiedzi tutaj wspomnieć zaskakujące zasady promocji między podpisane i niepodpisane wartości, ale to wydaje się raczej problemem dotyczącym mieszania podpisane i niepodpisane wartości, a nie koniecznie wyjaśnić, dlaczego podpisane zmienne byłyby korzystniejsze niż unsigned zewnątrz mieszania scenariuszy.
Z mojego doświadczenia wynika, że poza mieszanymi porównaniami i zasadami promocji istnieją dwa główne powody, dla których wartości bez znaku są magnesami na błędy.
Wartości bez znaku mają nieciągłość na poziomie zero, najczęściej spotykaną wartość w programowaniu
Zarówno liczby całkowite bez znaku, jak i ze znakiem mają nieciągłości na swoich minimalnych i maksymalnych wartościach, gdzie zawijają się (bez znaku) lub powodują niezdefiniowane zachowanie (ze znakiem). Dla unsigned
tych punktów są na zero i UINT_MAX
. Dlaint
są na INT_MIN
i INT_MAX
. Typowe wartości INT_MIN
i INT_MAX
w systemie z 4-bajtowymi int
wartościami to -2^31
i 2^31-1
, aw takim systemie UINT_MAX
zazwyczaj jest 2^32-1
.
Podstawowym problemem wywołującym błąd unsigned
, który nie dotyczy tego int
jest to, że ma nieciągłość na poziomie zero . Zero jest oczywiście bardzo powszechną wartością w programach, wraz z innymi małymi wartościami, takimi jak 1,2,3. Powszechne jest dodawanie i odejmowanie małych wartości, zwłaszcza 1, w różnych konstrukcjach, a jeśli odejmiesz cokolwiek od unsigned
wartości i zdarzy się, że jest to zero, otrzymujesz ogromną wartość dodatnią i prawie pewien błąd.
Rozważmy, że kod iteruje po wszystkich wartościach w wektorze według indeksu z wyjątkiem ostatniej 0,5 :
for (size_t i = 0; i < v.size() - 1; i++) {
Działa to dobrze, dopóki pewnego dnia nie przejdziesz do pustego wektora. Zamiast wykonywać zerowe iteracje, otrzymasz v.size() - 1 == a giant number
1, a wykonasz 4 miliardy iteracji i prawie masz lukę przepełnienia bufora.
Musisz to napisać tak:
for (size_t i = 0; i + 1 < v.size(); i++) {
Można więc to „naprawić” w tym przypadku, ale tylko poprzez dokładne przemyślenie niepodpisanej natury size_t
. Czasami nie możesz zastosować powyższej poprawki ponieważ zamiast stałej masz jakąś zmienną offset, którą chcesz zastosować, która może być dodatnia lub ujemna: więc po której "stronie" porównania musisz je umieścić zależy od podpisu - teraz kod robi się naprawdę nieuporządkowany.
Podobny problem występuje z kodem, który próbuje iterować w dół do zera włącznie. Coś jak while (index-- > 0)
działa dobrze, ale pozornie odpowiednikwhile (--index >= 0)
nigdy nie kończy się dla wartości bez znaku. Twój kompilator może Cię ostrzec, gdy po prawej stronie jest dosłownie zero, ale z pewnością nie, jeśli jest to wartość określona w czasie wykonywania.
Kontrapunkt
Niektórzy mogą twierdzić, że wartości ze znakiem również mają dwie nieciągłości, więc po co wybierać bez znaku? Różnica polega na tym, że obie nieciągłości są bardzo (maksymalnie) dalekie od zera. Naprawdę uważam to za osobny problem „przepełnienia”, zarówno wartości ze znakiem, jak i bez znaku mogą przepełniać się przy bardzo dużych wartościach. W wielu przypadkach przepełnienie jest niemożliwe ze względu na ograniczenia możliwego zakresu wartości, a przepełnienie wielu 64-bitowych wartości może być fizycznie niemożliwe). Nawet jeśli to możliwe, prawdopodobieństwo wystąpienia błędu związanego z przepełnieniem jest często znikome w porównaniu z błędem „przy zera”, a przepełnienie występuje również w przypadku wartości bez znaku . Tak więc bez znaku łączy w sobie to, co najgorsze z obu światów: potencjalne przepełnienie z bardzo dużymi wartościami wielkości i nieciągłość na poziomie zera. Podpisany ma tylko ten pierwszy.
Wielu będzie argumentować, że „trochę tracisz” przy braku znaku. Często jest to prawdą - ale nie zawsze (jeśli chcesz przedstawić różnice między wartościami bez znaku, i tak stracisz ten bit: tak wiele 32-bitowych rzeczy i tak jest ograniczonych do 2 GiB lub będziesz mieć dziwną szarą strefę, w której powiedz plik może mieć 4 GiB, ale nie można używać niektórych interfejsów API na drugiej połowie 2 GiB).
Nawet w przypadkach, gdy niepodpisany kupuje trochę: to niewiele: gdybyś musiał obsługiwać więcej niż 2 miliardy „rzeczy”, prawdopodobnie wkrótce będziesz musiał wesprzeć ponad 4 miliardy.
Logicznie rzecz biorąc, wartości bez znaku są podzbiorem wartości ze znakiem
Matematycznie, wartości bez znaku (nieujemne liczby całkowite) są podzbiorem liczb całkowitych ze znakiem (zwanych po prostu _całkami). 2 . Jeszcze podpisane wartości naturalnie wyskoczyć operacji wyłącznie na niepodpisanych wartości, takich jak odejmowania. Można powiedzieć, że wartości bez znaku nie są zamknięte przy odejmowaniu. To samo nie dotyczy wartości ze znakiem.
Chcesz znaleźć „różnicę” między dwoma niepodpisanymi indeksami w pliku? Cóż, lepiej wykonaj odejmowanie we właściwej kolejności, bo inaczej otrzymasz złą odpowiedź. Oczywiście często potrzebujesz sprawdzenia działania, aby określić właściwą kolejność! Gdy mamy do czynienia z wartościami bez znaku jako liczbami, często stwierdzamy, że (logicznie) podpisane wartości i tak pojawiają się, więc równie dobrze można zacząć od znaku ze znakiem.
Kontrapunkt
Jak wspomniano w przypisie (2) powyżej, podpisane wartości w C ++ nie są w rzeczywistości podzbiorem wartości bez znaku o tym samym rozmiarze, więc wartości bez znaku mogą reprezentować taką samą liczbę wyników, jak wartości ze znakiem.
To prawda, ale zakres jest mniej przydatny. Rozważ odejmowanie i liczby bez znaku w zakresie od 0 do 2N oraz liczby ze znakiem w zakresie od -N do N. Arbitralne odejmowania dają wyniki w zakresie od -2N do 2N w _w obu przypadkach, a każdy typ liczb całkowitych może reprezentować tylko połowa tego. Okazuje się, że region skupiony wokół zera od -N do N jest zwykle dużo bardziej przydatny (zawiera więcej rzeczywistych wyników w kodzie świata rzeczywistego) niż zakres od 0 do 2 N. Rozważ dowolny typowy rozkład inny niż jednorodny (log, zipfian, normalny, cokolwiek) i rozważ odjęcie losowo wybranych wartości z tego rozkładu: o wiele więcej wartości kończy się w [-N, N] niż [0, 2N] (w istocie, wynikowy rozkład jest zawsze wyśrodkowany na zero).
64-bit zamyka drzwi z wielu powodów, dla których warto używać wartości ze znakiem jako liczb
Myślę, że powyższe argumenty były już przekonujące dla wartości 32-bitowych, ale przypadki przepełnienia, które wpływają zarówno na podpisane, jak i niepodpisane przy różnych progach, tak występuje dla wartości 32-bitowych, ponieważ „2000000000” to numer, który może przekroczona o wiele wielkości abstrakcyjne i fizyczne (miliardy dolarów, miliardy nanosekund, tablice z miliardami elementów). Więc jeśli ktoś jest wystarczająco przekonany przez podwojenie dodatniego zakresu dla wartości bez znaku, może udowodnić, że przepełnienie ma znaczenie i nieco faworyzuje brak znaku.
Poza wyspecjalizowanymi domenami 64-bitowe wartości w dużej mierze eliminują ten problem. Podpisane wartości 64-bitowe mają górny zakres 9 223 372 036 854 775 807 - ponad dziewięć trylionów . To dużo nanosekund (około 292 lat) i dużo pieniędzy. Jest to również większa tablica niż jakikolwiek komputer, który prawdopodobnie będzie miał pamięć RAM w spójnej przestrzeni adresowej przez długi czas. Więc może 9 kwintylionów wystarczy każdemu (na razie)?
Kiedy używać wartości bez znaku
Zwróć uwagę, że przewodnik po stylach nie zabrania ani nawet nie odradza używania liczb bez znaku. Kończy się:
Nie używaj typu bez znaku tylko po to, aby zapewnić, że zmienna jest nieujemna.
Rzeczywiście, zmienne bez znaku mają dobre zastosowania:
Gdy chcesz traktować liczbę N-bitową nie jako liczbę całkowitą, ale po prostu jako „worek bitów”. Na przykład jako maska bitowa lub mapa bitowa lub N wartości logicznych lub cokolwiek innego. To zastosowanie często idzie w parze z typami o stałej szerokości, takimi jak uint32_t
i, uint64_t
ponieważ często chcesz znać dokładny rozmiar zmiennej. Wskazówką, że dana zmienna zasługuje na to leczenie jest to, że działają tylko na nim z bitowe operatorów takich jak ~
, |
, &
, ^
, >>
i tak dalej, a nie z operacji arytmetycznych, takich jak +
, -
, *
, /
etc.
Bez znaku jest tutaj idealne, ponieważ zachowanie operatorów bitowych jest dobrze zdefiniowane i znormalizowane. Podpisane wartości mają kilka problemów, takich jak niezdefiniowane i nieokreślone zachowanie podczas przesuwania oraz nieokreślona reprezentacja.
Kiedy faktycznie potrzebujesz arytmetyki modularnej. Czasami faktycznie potrzebujesz arytmetyki modularnej 2 ^ N. W takich przypadkach „przepełnienie” jest funkcją, a nie błędem. Wartości bez znaku dają ci to, czego chcesz, ponieważ są zdefiniowane do używania arytmetyki modularnej. Podpisanych wartości nie można w ogóle (łatwo i wydajnie) wykorzystać, ponieważ mają one nieokreśloną reprezentację, a przepełnienie jest niezdefiniowane.
0.5 Po napisaniu tego zdałem sobie sprawę, że jest to prawie identyczne z przykładem Jaroda , którego nie widziałem - i nie bez powodu jest to dobry przykład!
1 Mówimy size_t
tutaj, więc zwykle 2 ^ 32-1 w systemie 32-bitowym lub 2 ^ 64-1 w systemie 64-bitowym.
2 W C ++ tak nie jest, ponieważ wartości bez znaku zawierają więcej wartości na górnym końcu niż odpowiadający im typ ze znakiem, ale istnieje podstawowy problem polegający na tym, że manipulowanie wartościami bez znaku może skutkować (logicznie) podpisanymi wartościami, ale nie ma odpowiedniego problemu z wartościami ze znakiem (ponieważ podpisane wartości zawierają już wartości bez znaku).
unsigned int x = 0; --x;
i zobacz, cox
się stanie. Bez kontroli limitów rozmiar może nagle uzyskać nieoczekiwaną wartość, która może łatwo doprowadzić do UB.