Czy użycie int bez podpisu zamiast ze znakiem jest bardziej prawdopodobne, że spowoduje błędy? Czemu?

82

W przewodniku Google C ++ Style Guide na temat „Unsigned Integers” jest to sugerowane

Ze względu na przypadek historyczny standard C ++ używa również liczb całkowitych bez znaku do reprezentowania rozmiaru kontenerów - wielu członków organizacji normalizacyjnej uważa, że ​​jest to błąd, ale na tym etapie praktycznie nie można tego naprawić. Fakt, że arytmetyka bez znaku nie modeluje zachowania prostej liczby całkowitej, ale jest definiowana przez standard modelowania arytmetyki modularnej (zawijanie po przepełnieniu / niedomiarze) oznacza, że ​​kompilator nie może zdiagnozować znaczącej klasy błędów.

Co jest nie tak z arytmetyką modularną? Czy nie jest to oczekiwane zachowanie niepodpisanego int?

Jakiego rodzaju błędów (znaczącej klasy) dotyczy przewodnik? Przepełnione błędy?

Nie używaj typu bez znaku tylko po to, aby zapewnić, że zmienna jest nieujemna.

Jednym z powodów, dla których przychodzi mi do głowy użycie signed int zamiast unsigned int, jest to, że jeśli przepełnia (do wartości ujemnej), łatwiej jest go wykryć.

user7586189
źródło
4
Spróbuj zrobić unsigned int x = 0; --x;i zobacz, co xsię stanie. Bez kontroli limitów rozmiar może nagle uzyskać nieoczekiwaną wartość, która może łatwo doprowadzić do UB.
Jakiś programista,
33
Przynajmniej niepodpisane przepełnienie ma dobrze zdefiniowane zachowanie i daje oczekiwane rezultaty.
user7860670
35
W niezwiązanej z Twoim pytaniem wiadomości (ale nie z przewodnikiem po stylach Google), jeśli trochę poszukasz, znajdziesz (czasami słuszną) krytykę przewodników stylów Google. Nie traktuj ich jako ewangelii.
Jakiś programista,
18
Z drugiej strony intprzepełnienie i niedomiar to UB. Jest mniej prawdopodobne, że doświadczysz sytuacji, w której a intspróbuje wyrazić wartość, której nie może, niż sytuacja, która zmniejsza się o unsigned intwartość poniżej zera, ale ludzie, którzy byliby zaskoczeni zachowaniem unsigned intarytmetyki, to ludzie, którzy również mogą napisz kod, który spowodowałby intprzepełnienie związane z UB, takie jak użycie a < a + 1do sprawdzania przepełnienia.
François Andrieux
12
Jeśli przepełnienie liczby całkowitej bez znaku jest dobrze zdefiniowane. Jeśli liczba całkowita ze znakiem przepełnia się, jest to niezdefiniowane zachowanie. Wolę dobrze zdefiniowane zachowanie, ale jeśli Twój kod nie obsługuje przepełnionych wartości, jesteś zgubiony z obydwoma. Różnica polega na tym, że dla podpisanego jesteś już stracony dla operacji przepełnienia, dla bez znaku w poniższym kodzie. Zgadzam się tylko, że jeśli potrzebujesz wartości ujemnych, typ liczby całkowitej bez znaku jest oczywiście złym wyborem.
zbyt szczery dla tej strony

Odpowiedzi:

71

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 unsignedtych punktów są na zero i UINT_MAX. Dlaint są na INT_MINi INT_MAX. Typowe wartości INT_MINi INT_MAXw systemie z 4-bajtowymi intwartościami to -2^31i 2^31-1, aw takim systemie UINT_MAXzazwyczaj jest 2^32-1.

Podstawowym problemem wywołującym błąd unsigned, który nie dotyczy tego intjest 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 unsignedwartoś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++) { // do something }

Działa to dobrze, dopóki pewnego dnia nie przejdziesz do pustego wektora. Zamiast wykonywać zerowe iteracje, otrzymasz v.size() - 1 == a giant number1, 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++) { // do something }

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_ti, uint64_tponieważ 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_ttutaj, 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).

BeeOnRope
źródło
10
Zgadzam się ze wszystkim, co opublikowałeś, ale „64 bity powinny wystarczyć dla wszystkich” z pewnością wydaje się zbyt bliskie „640k powinno wystarczyć dla wszystkich”.
Andrew Henle
6
@Andrew - tak, starannie dobrałem słowa :).
BeeOnRope
4
„Wersja 64-bitowa zamyka drzwi dla wartości bez znaku” -> Nie zgadzam się. Niektóre zadania programowania liczb całkowitych są proste i nie wymagają liczenia i nie wymagają wartości ujemnych, ale wymagają potęgi 2 szerokości: hasła, szyfrowanie, grafika bitowa, korzyści z matematyką bez znaku. Wiele pomysłów tutaj wskazuje, dlaczego kod mógłby używać matematyki ze znakiem, jeśli jest to możliwe, ale nie pozwala uczynić niepodpisanego typu bezużytecznym i zamknąć drzwi.
chux - Przywróć Monikę
2
@Deduplicator - tak, pominąłem to, ponieważ wygląda mniej więcej jak krawat. Po stronie otaczającego mod-2 ^ N bez znaku masz przynajmniej zdefiniowane zachowanie i żadne nieoczekiwane „optymalizacje” nie pojawią się. Po stronie UB, każde przepełnienie podczas arytmetyki na niepodpisanym lub ze znakiem jest prawdopodobnie błędem w przytłaczającej większości przypadków (poza nielicznymi, którzy oczekują arytmetyki modów), a kompilatory zapewniają takie opcje, -ftrapvktóre mogą przechwytywać wszystkie przepełnienia ze znakiem, ale nie wszystkie przepełnienia bez znaku. Wpływ na wydajność nie jest taki zły, więc -ftrapvw niektórych scenariuszach może być rozsądna kompilacja .
BeeOnRope
2
@BeeOnRope That's about the age of the universe measured in nanoseconds.Wątpię w to. Wszechświat jest o 13.7*10^9 yearsstarym, który jest 4.32*10^17 slub 4.32*10^26 ns. Aby reprezentować 4.32*10^26jako int, potrzebujesz przynajmniej 90 bits. 9,223,372,036,854,775,807 nstylko o 292.5 years.
Ozyrys
37

Jak wspomniano, mieszanie unsignedi signedmoże prowadzić do nieoczekiwanego zachowania (nawet jeśli jest dobrze zdefiniowane).

Załóżmy, że chcesz iterować po wszystkich elementach wektora z wyjątkiem ostatnich pięciu, możesz niepoprawnie napisać:

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

Załóżmy v.size() < 5więc, jak v.size()jest unsigned, s.size() - 5będzie to bardzo duża liczba, a więc i < v.size() - 5byłoby truebardziej oczekiwanym zakresie wartości i. A UB dzieje się wtedy szybko (raz poza zasięgiem i >= v.size())

Jeśli v.size()zwróciłby wartość ze znakiem, to s.size() - 5byłby ujemny, aw powyższym przypadku warunek byłby natychmiast fałszywy.

Z drugiej strony indeks powinien znajdować się między, [0; v.size()[więc unsignedma sens. Signed ma również swój własny problem jako UB z przepełnieniem lub zachowaniem zdefiniowanym przez implementację dla przesunięcia w prawo ujemnej liczby ze znakiem, ale rzadszym źródłem błędów dla iteracji.

Jarod42
źródło
2
Chociaż sam używam liczb ze znakiem, kiedy tylko mogę, nie sądzę, aby ten przykład był wystarczająco mocny. Ktoś, kto od dawna używa liczb bez znaku, z pewnością zna ten idiom: zamiast tego i<size()-Xnależy pisać i+X<size(). Jasne, trzeba o tym pamiętać, ale moim zdaniem nie jest to takie trudne.
geza,
8
To, co mówisz, to po prostu znajomość języka i reguł przymusu między typami. Nie widzę, jak to się zmienia, czy używa się podpisu lub niepodpisu, jak zadaje pytanie. Nie żebym w ogóle polecał używanie podpisu, jeśli nie ma potrzeby stosowania wartości ujemnych. Zgadzam się z @geza, używaj podpisu tylko wtedy, gdy jest to konieczne. To sprawia, że ​​przewodnik Google jest w najlepszym razie wątpliwy . Imo, to zła rada.
zbyt szczery dla tej strony
2
@toohonestforthissite Chodzi o to, że zasady są tajemnicze, ciche i główne przyczyny błędów. Używanie wyłącznie podpisanych typów do arytmetyki zwalnia Cię z tego problemu. Tak przy okazji, używanie typów bez znaku w celu wymuszania wartości dodatnich jest jednym z najgorszych ich nadużyć.
Przechodzień
2
Na szczęście nowoczesne kompilatory i środowiska IDE ostrzegają przed mieszaniem liczb ze znakiem i bez znaku w wyrażeniu.
Alexey B.
5
@PasserBy: Jeśli nazywasz je tajemniczymi, musisz dodać promocje na liczby całkowite i UB dla przepełnienia podpisanych typów tajemniczych. A bardzo często operator sizeof zwraca niepodpisane i tak, więc trzeba o nich wiedzieć. Powiedział, że: jeśli nie chcesz uczyć się szczegółów języka, po prostu nie używaj C ani C ++! Biorąc pod uwagę, że promocje Google znikają, może to jest dokładnie ich cel. Dni „nie bądź zły” już dawno minęły…
zbyt szczere dla tej strony
20

Jednym z najbardziej niepokojących przykładów błędu jest MIESZANIE wartości ze znakiem i bez znaku:

#include <iostream>
int main()  {
    auto qualifier = -1 < 1u ? "makes" : "does not make";
    std::cout << "The world " << qualifier << " sense" << std::endl;
}

Wyjście:

Świat nie ma sensu

Jeśli nie masz trywialnej aplikacji, nieuniknione jest, że skończysz z niebezpiecznymi mieszankami między wartościami podpisanymi i niepodpisanymi (powodującymi błędy w czasie wykonywania) lub jeśli włączysz ostrzeżenia i zrobisz z nich błędy w czasie kompilacji, skończysz z wieloma static_casts w Twoim kodzie. Dlatego najlepiej jest używać wyłącznie liczb całkowitych ze znakiem dla typów do matematycznego lub logicznego porównania. Używaj tylko bez znaku dla masek bitowych i typów reprezentujących bity.

Modelowanie typu bez znaku w oparciu o oczekiwaną dziedzinę wartości twoich liczb jest złym pomysłem. Większość liczb jest bliżej 0 niż 2 miliardów, więc w przypadku typów bez znaku wiele wartości jest bliżej krawędzi prawidłowego zakresu. Co gorsza, ostateczna wartość może znajdować się w znanym dodatnim zakresie, ale podczas obliczania wyrażeń wartości pośrednie mogą być niedostateczne i jeśli są używane w postaci pośredniej, mogą być BARDZO błędnymi wartościami. Wreszcie, nawet jeśli oczekuje się, że wartości zawsze będą dodatnie, nie oznacza to, że nie będą one oddziaływać z innymi zmiennymi, które mogą być ujemne, i kończy się to wymuszoną sytuacją mieszania typów ze znakiem i bez znaku, czyli najgorsze miejsce.

Chris Uzdavinis
źródło
8
Modelowanie typu bez znaku w oparciu o oczekiwaną dziedzinę wartości twoich liczb jest złym pomysłem *, jeśli nie traktujesz niejawnych konwersji jako ostrzeżeń i jesteś zbyt leniwy, aby używać odpowiednich rzutów typów. * Modelowanie typów według ich oczekiwanego prawidłowego wartości są całkowicie rozsądne, ale nie w C / C ++ z wbudowanymi typami.
villasv
1
@ user7586189 Dobrą praktyką jest uniemożliwienie utworzenia wystąpienia nieprawidłowych danych, więc posiadanie zmiennych tylko dodatnich dla rozmiarów jest całkowicie uzasadnione. Ale nie możesz dostroić wbudowanych typów C / C ++, aby domyślnie zabronić złych rzutów, takich jak ta w tej odpowiedzi, a ważność kończy się na kimś innym. Jeśli jesteś w języku z bardziej rygorystycznymi rzutami (nawet między wbudowanymi), modelowanie oczekiwanej domeny jest całkiem dobrym pomysłem.
villasv
1
Uwaga, to nie wspomina kręcenie ostrzeżenia i ustawiając je na błędy, ale nie każdy ma. Nadal nie zgadzam się @villasv z twoim stwierdzeniem na temat wartości modelowania. Wybierając niepodpisane, RÓWNIEŻ niejawnie modelujesz każdą inną wartość, z którą może się zetknąć, bez zbytniego przewidywania, co to będzie. I prawie na pewno się mylę.
Chris Uzdavinis,
1
Modelowanie z myślą o domenie to dobra rzecz. Używanie unsigned do modelowania domeny NIE JEST. (Signed vs unsigned powinien być wybierany na podstawie typów użycia , a nie zakresu wartości , chyba że nie można zrobić inaczej.)
Chris Uzdavinis
2
Gdy baza kodu zawiera kombinację wartości podpisanych i niepodpisanych, po pojawieniu się ostrzeżeń i promowaniu ich na błędy, kod kończy się zaśmieceniem static_casts, aby konwersja była jawna (ponieważ matematyka nadal musi zostać wykonana). Nawet jeśli jest poprawna, jest podatny na błędy, trudniejszy w obsłudze i trudniejszy do odczytania.
Chris Uzdavinis,
11

Dlaczego użycie niepodpisanego int jest bardziej prawdopodobne, aby spowodować błędy niż użycie podpisanego int?

Użycie typu bez znaku nie powoduje większych błędów niż użycie typu podpisanego z pewnymi klasami zadań.

Użyj odpowiedniego narzędzia do pracy.

Co jest nie tak z arytmetyką modularną? Czy nie jest to oczekiwane zachowanie niepodpisanego int?
Dlaczego użycie niepodpisanego int jest bardziej prawdopodobne, aby spowodować błędy niż użycie podpisanego int?

Jeśli zadanie jest dobrze dopasowane: nic złego. Nie, raczej nie.

Algorytm bezpieczeństwa, szyfrowania i uwierzytelniania opiera się na niepodpisanej modułowej matematyce.

Algorytmy kompresji / dekompresji, a także różne formaty graficzne przynoszą korzyści i są mniej błędne dzięki niepodpisanej matematyce.

Za każdym razem, operatory bitowe mądry i przesunięcia są używane, niepodpisane operacje nie się pokręcić się problematyką znak rozciągnięcia podpisanej matematyki.


Podpisana matematyka liczb całkowitych ma intuicyjny wygląd i jest łatwo zrozumiała dla wszystkich, w tym dla osób uczących się kodowania. C / C ++ nie był pierwotnie celem, ani teraz nie powinien być językiem wprowadzającym. Do szybkiego kodowania wykorzystującego siatki zabezpieczające przed przepełnieniem lepiej nadają się inne języki. W przypadku szybkiego kodu Lean C zakłada, że ​​programiści wiedzą, co robią (mają doświadczenie).

Dzisiejszą pułapką matematyki podpisanej jest wszechobecna wersja 32-bitowa, intktóra przy tak wielu problemach jest wystarczająco szeroka dla typowych zadań bez sprawdzania zakresu. Prowadzi to do samozadowolenia, że ​​przepełnienie nie jest kodowane. Zamiast tego for (int i=0; i < n; i++) int len = strlen(s);jest postrzegany jako OK, ponieważ nzakłada się, że < INT_MAXi ciągi nigdy nie będą zbyt długie, zamiast być chronione w pełnym zakresie w pierwszym przypadku lub przy użyciu size_t, unsigneda nawet long longw drugim.

C / C ++ opracowany w erze, która obejmowała zarówno 16-bitowe, jak i 32-bitowe, inta dodatkowy bit, który zapewnia 16-bitowy bez znaku, size_tbył znaczący. Trzeba było zwrócić uwagę na problemy z przepełnieniem, intczy to lub unsigned.

Przy 32-bitowych (lub szerszych) aplikacjach Google na int/unsignedplatformach innych niż 16-bitowe , daje brak uwagi na +/- przepełnienie z intuwagi na jego duży zasięg. Ma to sens do takich zastosowań, aby zachęcić intponad unsigned. Jednak intmatematyka nie jest dobrze chroniona.

Wąskie 16-bitowe int/unsignedproblemy dotyczą obecnie wybranych aplikacji wbudowanych.

Wytyczne Google mają zastosowanie do kodu, który piszą dzisiaj. Nie jest to ostateczna wytyczna dla szerszego zakresu kodu C / C ++.


Jednym z powodów, dla których przychodzi mi do głowy użycie signed int zamiast unsigned int, jest to, że jeśli przepełnia (do wartości ujemnej), łatwiej jest go wykryć.

W C / C ++ przepełnienie matematyczne ze znakiem int jest niezdefiniowanym zachowaniem, a więc z pewnością nie jest łatwiejsze do wykrycia niż zdefiniowane zachowanie matematyki bez znaku .


Jak dobrze skomentował @Chris Uzdavinis , mieszanie podpisów i niepodpisów jest najlepiej unikane przez wszystkich (szczególnie początkujących) i w inny sposób ostrożnie kodowane w razie potrzeby.

chux - Przywróć Monikę
źródło
2
Dobrze zauważysz, że an intnie modeluje również zachowania „rzeczywistej” liczby całkowitej. Niezdefiniowane zachowanie przy przepełnieniu nie jest sposobem, w jaki matematyk myśli o liczbach całkowitych: nie ma możliwości „przepełnienia” abstrakcyjną liczbą całkowitą. Ale to są jednostki magazynowe maszyn, a nie liczby matematyka.
tchrist
1
@tchrist: Zachowanie bez znaku przy przepełnieniu to sposób, w jaki matematyk pomyślałby o abstrakcyjnym algebraicznym pierścieniu modulacji liczb całkowitych (type_MAX + 1).
supercat
Jeśli używasz gcc, signed intprzepełnienie jest łatwe do wykrycia (za pomocą -ftrapv), podczas gdy niepodpisane „przepełnienie” jest trudne do wykrycia.
anatolyg
5

Mam pewne doświadczenie z przewodnikiem stylistycznym Google, znanym również jako Przewodnik autostopowicza po szalonych dyrektywach od złych programistów, którzy dostali się do firmy dawno temu. Ta konkretna wskazówka jest tylko jednym z przykładów dziesiątek szalonych zasad w tej książce.

Błędy występują tylko w przypadku typów bez znaku, jeśli spróbujesz wykonać z nimi operacje arytmetyczne (zobacz przykład Chrisa Uzdavinisa powyżej), innymi słowy, jeśli używasz ich jako liczb. Typy bez znaku nie są przeznaczone do przechowywania ilości liczbowych, służą do przechowywania zliczeń, takich jak rozmiar kontenerów, które nigdy nie mogą być ujemne i mogą i powinny być używane do tego celu.

Pomysł wykorzystania typów arytmetycznych (takich jak liczby całkowite ze znakiem) do przechowywania rozmiarów kontenerów jest idiotyczny. Czy użyłbyś podwójnego do przechowywania rozmiaru listy? To, że w Google są ludzie, którzy przechowują rozmiary kontenerów przy użyciu typów arytmetycznych i wymagają od innych, aby robili to samo, mówi coś o firmie. Jedną rzeczą, którą zauważyłem w przypadku takich nakazów, jest to, że im są głupsi, tym bardziej muszą być surowymi regułami typu „zrób to albo jesteś zwolniony”, ponieważ w przeciwnym razie ludzie o zdrowym rozsądku zignorowaliby tę regułę.

Tyler Durden
źródło
Chociaż rozumiem twój dryf, złożone instrukcje ogólne praktycznie wyeliminowałyby operacje bitowe, gdyby unsignedtypy mogły przechowywać tylko liczby i nie byłyby używane w arytmetyce. Więc część „Szalone dyrektywy od złych programistów” ma więcej sensu.
David C. Rankin
@ DavidC.Rankin Proszę nie traktować tego jako „ogólnego” oświadczenia. Oczywiście istnieje wiele uzasadnionych zastosowań liczb całkowitych bez znaku (takich jak przechowywanie wartości bitowych).
Tyler Durden
Tak, tak - nie zrobiłem tego, dlatego powiedziałem „rozumiem twój dryf”
David C. Rankin
1
Liczby są często porównywane do rzeczy, na których wykonano obliczenia arytmetyczne, takich jak indeksy. Sposób, w jaki C obsługuje porównania obejmujące liczby ze znakiem i bez znaku, może prowadzić do wielu dziwnych dziwactw. Z wyjątkiem sytuacji, w których górna wartość licznika mieściłaby się w typie bez znaku, ale nie odpowiadającym typowi ze znakiem (powszechne w czasach, gdy intbyło 16 bitów, ale o wiele mniej dzisiaj), lepiej mieć liczniki, które zachowują się jak liczby.
supercat
1
„Błędy pojawiają się w przypadku typów bez znaku tylko wtedy, gdy próbujesz wykonywać na nich operacje arytmetyczne” - co zdarza się cały czas. „Pomysł wykorzystania typów arytmetycznych (takich jak liczby całkowite ze znakiem) do przechowywania rozmiarów kontenerów jest idiotyczny” - tak nie jest i komisja C ++ uważa, że ​​użycie size_t jest historycznym błędem. Powód? Niejawne konwersje.
Átila Neves
1

Używanie typów bez znaku do reprezentowania wartości nieujemnych ...

  • jest bardziej prawdopodobne że spowoduje błędy związane z promocją typów, gdy używa się wartości ze znakiem i bez znaku, co inne odpowiedzi pokazują i szczegółowo omawiają, ale
  • jest mniej prawdopodobne, aby przyczyna błędów związanych z wyborem typów domen zdolnych do reprezentującą undersirable / niedozwolone wartości. W niektórych miejscach zakładasz, że wartość należy do domeny i możesz uzyskać nieoczekiwane i potencjalnie niebezpieczne zachowanie, gdy w jakiś sposób wkradnie się inna wartość.

Wytyczne Google dotyczące kodowania kładą nacisk na pierwszy rodzaj rozważań. Inne zestawy wytycznych, takie jak C ++ Core Guidelines , kładą większy nacisk na drugą kwestię. Weźmy na przykład pod uwagę Podstawową wytyczną I.12 :

I.12: Zadeklaruj wskaźnik, który nie może być zerowy jako not_null

Powód

Aby pomóc uniknąć wyłuskiwania odwołań do błędów nullptr. Aby poprawić wydajność, unikając zbędnych sprawdzeń nullptr.

Przykład

int length(const char* p);            // it is not clear whether length(nullptr) is valid
length(nullptr);                      // OK?
int length(not_null<const char*> p);  // better: we can assume that p cannot be nullptr
int length(const char* p);            // we must assume that p can be nullptr

Określając zamiar w źródle, implementatorzy i narzędzia mogą zapewnić lepszą diagnostykę, na przykład znajdowanie niektórych klas błędów za pomocą analizy statycznej, i przeprowadzać optymalizacje, takie jak usuwanie gałęzi i testów zerowych.

Oczywiście można argumentować za non_negativeopakowaniem dla liczb całkowitych, który pozwala uniknąć obu kategorii błędów, ale miałoby to swoje własne problemy ...

einpoklum
źródło
0

Oświadczenie Google dotyczy używania unsigned jako typu rozmiaru dla kontenerów . Natomiast pytanie wydaje się bardziej ogólne. Pamiętaj o tym podczas czytania.

Ponieważ większość dotychczasowych odpowiedzi reagowała na stwierdzenie google, a mniej na większe pytanie, zacznę od odpowiedzi o ujemnych rozmiarach pojemników, a następnie spróbuję przekonać każdego (beznadziejnego, wiem ...), że brak znaku jest dobry.

Podpisane rozmiary kontenerów

Załóżmy, że ktoś zakodował błąd, którego wynikiem jest ujemny indeks kontenera. Rezultatem jest niezdefiniowane zachowanie lub wyjątek / naruszenie zasad dostępu. Czy to naprawdę lepsze niż uzyskanie niezdefiniowanego zachowania lub naruszenia wyjątku / dostępu, gdy typ indeksu był niepodpisany? Myśle że nie.

Jest klasa ludzi, którzy uwielbiają rozmawiać o matematyce i tym, co w tym kontekście jest „naturalne”. W jaki sposób typ całkowy z liczbą ujemną może być naturalny do opisania czegoś, co jest z natury> = 0? Często używasz tablic o ujemnych rozmiarach? IMHO, zwłaszcza ludzie o skłonnościach matematycznych, uznaliby tę niedopasowanie semantyki (typ rozmiaru / indeksu mówi, że jest możliwy negatyw, podczas gdy tablica o rozmiarze ujemnym jest trudna do wyobrażenia) irytująca.

Pozostaje więc tylko pytanie, czy - jak stwierdzono w komentarzu google - kompilator rzeczywiście mógłby aktywnie pomagać w znajdowaniu takich błędów. I nawet lepiej niż alternatywa, która byłaby chronionymi niedopływem liczb całkowitych bez znaku (zestaw x86-64 i prawdopodobnie inne architektury mają środki do osiągnięcia tego, tylko C / C ++ nie używa tych środków). Jedynym sposobem, w jaki mogę to pojąć, jest to, czy kompilator automatycznie dodał testy czasu wykonywania ( if (index < 0) throwOrWhatever) lub w przypadku działań w czasie kompilacji generuje wiele potencjalnie fałszywie dodatnich ostrzeżeń / błędów. „Indeks dla tego dostępu do tablicy może być ujemny”. Mam wątpliwości, to byłoby pomocne.

Ponadto osoby, które faktycznie piszą kontrole czasu wykonywania dla swoich indeksów tablic / kontenerów, wymagają więcej pracy związanej z liczbami całkowitymi ze znakiem. Zamiast pisać if (index < container.size()) { ... }masz teraz napisać: if (index >= 0 && index < container.size()) { ... }. Dla mnie wygląda na pracę przymusową, a nie na poprawę ...

Języki bez niepodpisanych typów są do niczego ...

Tak, to jest atak na Javę. Teraz pochodzę z wbudowanego programowania i dużo pracowaliśmy z magistralami polowymi, w których operacje binarne (i, lub, xor, ...) i nieco mądre składanie wartości to dosłownie chleb powszedni. W przypadku jednego z naszych produktów, my - a raczej klient - chcieliśmy portu java ... i siedziałem naprzeciwko bardzo kompetentnego na szczęście gościa, który zajmował się portem (odmówiłem ...). Próbował zachować spokój ... i cierpieć w ciszy ... ale ból był tam, nie mógł przestać przeklinać po kilku dniach ciągłego zajmowania się podpisanymi wartościami całkowitymi, które POWINNY być niepodpisane ... Nawet pisanie testów jednostkowych dla te scenariusze są bolesne i ja osobiście uważam, że java byłoby lepiej, gdyby pominęli liczby całkowite ze znakiem i zaoferowali tylko bez znaku ... przynajmniej wtedy nie musisz się przejmować rozszerzeniami znaków itp ...

To moje 5 centów w tej sprawie.

BitTickler
źródło