Jakie są najlepsze praktyki dotyczące niepodpisanych ints?

43

Używam wszędzie niepodpisanych int i nie jestem pewien, czy powinienem. Może to być od kolumny identyfikatora klucza podstawowego bazy danych do liczników itp. Jeśli liczba nigdy nie powinna być ujemna, zawsze będę używał znaku int bez znaku.

Jednak zauważam z kodu innego, że nikt inny tego nie robi. Czy przeoczyłem coś kluczowego?

Edycja: Odkąd to pytanie zauważyłem również, że w C zwracanie ujemnych wartości błędów jest powszechne, a nie zgłaszanie wyjątków jak w C ++.

wting
źródło
26
Tylko uważaj na for(unsigned int n = 10; n >= 0; n --)(pętle nieskończenie)
Chris Burt-Brown
3
W C i C ++ niepodpisane inty mają dokładnie zdefiniowane zachowanie związane z przepełnieniem (moduł 2 ^ n). Podpisane ints nie. Optymalizatory coraz częściej wykorzystują to niezdefiniowane zachowanie związane z przepełnieniem, co w niektórych przypadkach prowadzi do zaskakujących wyników.
Steve314,
2
Dobre pytanie! Ja też kiedyś miałem pokusę, aby używać ograniczenia tint, ale odkryłem, że ryzyko / niedogodność przeważają nad jakąkolwiek korzyścią / wygodą. Większość bibliotek, jak powiedziałeś, akceptuje regularne ints tam, gdzie zrobiłby to uint. Utrudnia to pracę, ale rodzi też pytanie: czy warto? W praktyce (przy założeniu, że nie zajmujesz się głupotami), rzadko kiedy wartość -218 przychodzi tam, gdzie oczekiwana jest wartość dodatnia. To -218 musiało skądś pochodzić, prawda? i możesz prześledzić jego pochodzenie. Zdarza się rzadko. Korzystaj z asercji, wyjątków, umów kodowych, aby ci pomóc.
Job
@William Ting: Jeśli chodzi tylko o C / C ++, należy dodać odpowiednie tagi do pytania.
CesarGon,
2
@Chris: Jak znaczący jest problem nieskończonej pętli w rzeczywistości? Chodzi mi o to, że jeśli pojawi się w wersji, kod oczywiście nie został przetestowany. Nawet jeśli potrzebujesz kilku godzin na debugowanie przy pierwszym wystąpieniu tego błędu, po raz drugi powinieneś wiedzieć, czego szukać w pierwszej kolejności, gdy kod nie przestaje zapętlać.
Bezpieczne

Odpowiedzi:

28

Czy przeoczyłem coś kluczowego?

Gdy obliczenia obejmują zarówno podpisane, jak i niepodpisane typy, a także różne rozmiary, reguły promocji typów mogą być złożone i prowadzić do nieoczekiwanego zachowania .

Uważam, że jest to główny powód, dla którego Java pominęła niepodpisane typy int.

Michael Borgwardt
źródło
3
Innym rozwiązaniem byłoby wymaganie od ciebie ręcznego rzucania liczbami, stosownie do potrzeb. To wydaje się robić Go (choć trochę się z tym bawiłem) i bardziej podoba mi się podejście Javy.
Tikhon Jelvis
2
To był dobry powód, dla którego Java nie zawierała 64-bitowego typu bez znaku, a może porządny powód, aby nie uwzględniać 32-bitowego typu bez znaku [chociaż semantyka dodawania 32-bitowych wartości ze znakiem i bez znaku nie byłaby trudna ... taka operacja powinna po prostu dać wynik 64-bitowy ze znakiem]. Typy niepodpisane mniejsze niż intnie stanowiłyby jednak takiej trudności (ponieważ wszelkie obliczenia będą się promować int); Nie mam nic dobrego do powiedzenia na temat braku typu bajtu bez znaku.
supercat
17

Myślę, że Michael ma rację, ale IMO powoduje, że wszyscy używają int cały czas (szczególnie w for (int i = 0; i < max, i++), ponieważ nauczyliśmy się tego w ten sposób. Kiedy każdy przykład w książce „ jak się uczyć programowania ” używa intw forpętli, bardzo niewielu kiedykolwiek kwestionuje tę praktykę.

Innym powodem jest to, że intjest o 25% krótszy uinti wszyscy jesteśmy leniwi ... ;-)

Treb
źródło
2
Zgadzam się z kwestią edukacyjną. Wydaje się, że większość ludzi nigdy nie kwestionuje tego, co czytają: jeśli jest w książce, nie może być źle, prawda?
Matthieu M.,
1
Prawdopodobnie dlatego wszyscy używają Postfiksa ++podczas zwiększania, pomimo tego, że jego szczególne zachowanie jest rzadko potrzebne i może nawet prowadzić do bezcelowego odrzucania kopii, jeśli indeks pętli jest iteratorem lub innym nie fundamentalnym typem (lub kompilator jest naprawdę gęsty) .
underscore_d
Po prostu nie rób czegoś takiego jak „dla (uint i = 10; i> = 0; --i)”. Używanie tylko wartości całkowitych dla zmiennych pętli pozwala uniknąć tej możliwości.
David Thornley,
11

Kodowanie informacji o zakresie na typy jest dobrą rzeczą. Wymusza używanie rozsądnych liczb w czasie kompilacji.

Wydaje się, że wiele architektur ma wyspecjalizowane instrukcje postępowania z int-> floatkonwersjami. Konwersja z unsignedmoże być wolniejsza (trochę) .

Benjamin Bannier
źródło
8

Mieszanie typów podpisanych i niepodpisanych może wprowadzić Cię w świat bólu. I nie możesz używać wszystkich niepodpisanych typów, ponieważ napotkasz rzeczy, które albo mają prawidłowy zakres, który zawiera liczby ujemne, albo potrzebują wartości wskazującej błąd, a -1 jest najbardziej naturalne. Tak więc wynik netto jest taki, że wielu programistów używa wszystkich typów całkowitych ze znakiem.

David Schwartz
źródło
1
Być może lepszą praktyką jest nie mieszanie prawidłowych wartości ze wskazaniem błędu w tej samej zmiennej i stosowanie do tego osobnych zmiennych. To prawda, że ​​biblioteka standardowa C nie stanowi tutaj dobrego przykładu.
Zabezpiecz
7

Dla mnie typy dotyczą komunikacji. Używając jawnie int bez znaku mówisz mi, że podpisane wartości nie są prawidłowymi wartościami. To pozwala mi dodać pewne informacje podczas odczytywania kodu oprócz nazwy zmiennej. Idealnie byłbym typem anonimowym, który powiedziałby mi więcej, ale daje mi więcej informacji niż gdybyś używał ints wszędzie.

Niestety nie wszyscy są bardzo świadomi tego, co komunikuje ich kod, i prawdopodobnie jest to powód, dla którego wszędzie widzisz ints, nawet jeśli wartości są przynajmniej niepodpisane.

daramarak
źródło
4
Ale może chciałbym ograniczyć moje wartości tylko przez miesiąc do 1 do 12. Czy używam do tego innego typu? Co powiesz na miesiąc? Niektóre języki faktycznie pozwalają na ograniczenie takich wartości. Inne, takie jak .Net / C #, zawierają umowy na kod. Oczywiście, nieujemne liczby całkowite występują dość często, ale większość języków obsługujących ten typ nie obsługuje dalszych ograniczeń. Czy zatem należy stosować kombinację odcieni i sprawdzania błędów, czy po prostu robić wszystko poprzez sprawdzanie błędów? Większość bibliotek nie prosi o wskazanie, gdzie byłoby sensowne użycie jednego, dlatego używanie jednego i rzutowanie może być niewygodne.
Job
@ Job Powiedziałbym, że powinieneś stosować jakieś wymuszone przez kompilatora / tłumacza ograniczenia w twoich miesiącach. Może dać ci trochę podstaw do skonfigurowania, ale w przyszłości masz narzucone ograniczenia, które zapobiegają błędom i komunikują się znacznie wyraźniej, czego oczekujesz. Zapobieganie błędom i ułatwienie komunikacji są znacznie ważniejsze niż niedogodności podczas wdrażania.
daramarak
1
„Mogę ograniczyć moje wartości tylko na miesiąc do 1 do 12”. Jeśli masz skończony zestaw wartości, takich jak miesiące, powinieneś użyć typu wyliczenia, a nie surowych liczb całkowitych.
Josh Caswell
6

Używam unsigned intw C ++ głównie dla indeksów tablicowych i dla każdego licznika rozpoczynającego się od 0. Myślę, że dobrze jest powiedzieć wprost: „ta zmienna nie może być ujemna”.

quant_dev
źródło
14
Prawdopodobnie powinieneś używać do tego size_t w c ++
JohnB
2
Wiem, po prostu nie mogę się tym przejmować.
quant_dev
3

Powinieneś się tym przejmować, gdy masz do czynienia z liczbą całkowitą, która może faktycznie zbliżyć się lub przekroczyć granice podpisanej liczby całkowitej. Ponieważ dodatnia wartość maksymalna 32-bitowej liczby całkowitej wynosi 2 147 483 647, powinieneś użyć int bez znaku, jeśli wiesz, że to a) nigdy nie będzie ujemne, a b) może osiągnąć 2 147 483 648. W większości przypadków, w tym kluczy bazy danych i liczników, nigdy nawet nie podchodzę do tego rodzaju liczb, więc nie zawracam sobie głowy martwieniem się, czy bit znaku jest używany dla wartości liczbowej, czy też do wskazania znaku.

Powiedziałbym: użyj int, chyba że wiesz, że potrzebujesz int bez znaku.

Joel Etherton
źródło
2
Podczas pracy z wartościami, które mogą osiągnąć wartości maksymalne, należy rozpocząć sprawdzanie operacji pod kątem przepełnienia liczb całkowitych, niezależnie od znaku. Te kontrole są zwykle łatwiejsze dla typów niepodpisanych, ponieważ większość operacji ma dobrze zdefiniowane wyniki bez niezdefiniowanego zachowania i zachowanie zdefiniowane w ramach implementacji.
Zabezpiecz
3

Jest to kompromis między prostotą a niezawodnością. Im więcej błędów można wykryć w czasie kompilacji, tym bardziej niezawodne jest oprogramowanie. Różni ludzie i organizacje zajmują różne punkty w tym spektrum.

Jeśli kiedykolwiek będziesz programował w Adzie o wysokiej niezawodności, użyjesz nawet różnych typów dla zmiennych, takich jak odległość w stopach vs. odległość w metrach, a kompilator oznaczy go, jeśli przypadkowo przypiszesz jeden do drugiego. Jest to idealne rozwiązanie do programowania pocisku kierowanego, ale przesadzanie (gra słów), jeśli sprawdzasz poprawność formularza internetowego. W obu przypadkach nie musi być nic złego, o ile spełnia ono wymagania.

Karl Bielefeldt
źródło
2

Jestem skłonny zgodzić się z rozumowaniem Joela Ethertona, ale doszedłem do przeciwnego wniosku. Z mojego punktu widzenia, nawet jeśli wiesz, że jest mało prawdopodobne, aby liczby kiedykolwiek zbliżyły się do limitów podpisanego typu, jeśli wiesz, że liczby ujemne się nie zdarzą, to jest bardzo mało powodu, aby używać podpisanego wariantu typu.

Z tego samego powodu, dla którego w kilku wybranych przypadkach użyłem BIGINT(64-bitowej liczby całkowitej) zamiast INTEGER(32-bitowej liczby całkowitej) w tabelach programu SQL Server. Prawdopodobieństwo, że dane osiągną limit 32-bitowy w rozsądnym czasie, jest niewielkie, ale jeśli tak się stanie, konsekwencje w niektórych sytuacjach mogą być dość katastrofalne. Pamiętaj tylko, aby poprawnie mapować typy między językami, w przeciwnym razie skończysz z ciekawą dziwnością naprawdę daleko w dół drogi ...

To powiedziawszy, dla niektórych rzeczy, takich jak wartości klucza podstawowego bazy danych, podpisane lub niepodpisane, tak naprawdę nie ma znaczenia, ponieważ dopóki nie naprawisz ręcznie uszkodzonych danych lub czegoś podobnego, nigdy nie będziesz miał do czynienia z wartością bezpośrednio; to identyfikator, nic więcej. W takich przypadkach spójność jest prawdopodobnie ważniejsza niż dokładny wybór podpisu. W przeciwnym razie powstają kolumny z kluczem obcym, które są podpisane, i inne, które są niepodpisane, bez widocznego wzorca - lub znowu ta interesująca dziwność.

CVn
źródło
Jeśli pracujesz z danymi wyodrębnionymi z systemu SAP, zdecydowanie polecam BIGINT dla pól identyfikacyjnych (takich jak CustomerNumber, ArticleNumber itp.). Dopóki nikt nie używa ciągi alfanumeryczne jak identyfikatory, które są ... wzdycham
TREB
1

Zalecałbym, aby poza kontekstami przechowywania danych i wymiany danych ograniczonymi przestrzenią, ogólnie rzecz biorąc, należy używać podpisanych typów. W większości przypadków, gdy 32-bitowa liczba całkowita ze znakiem byłaby zbyt mała, ale 32-bitowa wartość bez znaku wystarczyłaby na dziś, nie potrwa długo, zanim 32-bitowa wartość bez znaku też nie będzie wystarczająco duża.

Podstawowymi czasami, w których należy używać typów bez znaku, są sytuacje, gdy albo łączy się wiele wartości w większą (np. Konwertuje cztery bajty na liczbę 32-bitową), albo rozkłada większe wartości na mniejsze (np. Przechowujemy liczbę 32-bitową jako cztery bajty ) lub gdy ma się pewną ilość, która ma się okresowo „przewracać” i trzeba sobie z tym poradzić (pomyśl o mierniku użyteczności publicznej; większość z nich ma wystarczającą liczbę cyfr, aby zapewnić, że nie przewrócą się między odczytami jeśli są czytane trzy razy w roku, ale nie na tyle, aby nie przewróciły się w okresie użytkowania miernika). Typy niepodpisane często mają wystarczającą „dziwność”, dlatego należy ich używać tylko w przypadkach, w których ich semantyka jest konieczna.

supercat
źródło
1
„Poleciłbym [...] ogólnie używać podpisanych typów”. Hm, zapomniałeś wspomnieć o zaletach podpisanych typów i podałeś tylko listę, kiedy używać niepodpisanych typów. „dziwność” ? Podczas gdy większość niepodpisanych operacji ma dobrze zdefiniowane zachowanie i wyniki, wprowadzasz niezdefiniowane i zdefiniowane zachowanie podczas używania podpisanych typów (przepełnienie, przesunięcie bitów, ...). Masz tutaj dziwną definicję „dziwności”.
Zabezpiecz
1
@Secure: „Dziwactwo”, o którym mówię, ma związek z semantyką operatorów porównania, szczególnie w operacjach obejmujących mieszane typy podpisane i niepodpisane. Masz rację, że zachowanie typów podpisanych jest niezdefiniowane, gdy używasz wartości wystarczająco dużych, aby się przelać, ale zachowanie typów niepodpisanych może być zaskakujące nawet w przypadku stosunkowo małych liczb. Na przykład (-3) + (1u) jest większe niż -1. Ponadto niektóre normalne matematyczne relacje asocjacyjne, które miałyby zastosowanie do liczb, nie dotyczą niepodpisanych. Na przykład (ab)> c nie oznacza (ac)> b.
supercat
1
@Secure: Chociaż prawdą jest, że nie zawsze można polegać na takim skojarzeniu z „dużymi” liczbami podpisanymi, zachowania te działają zgodnie z oczekiwaniami w przypadku liczb „małych” w stosunku do dziedziny liczb całkowitych ze znakiem. Natomiast wyżej wspomniany brak powiązania jest problematyczny z niepodpisanymi wartościami „2 3 1”. Nawiasem mówiąc, fakt, że podpisane zachowania mają niezdefiniowane zachowanie, gdy są używane poza granicami, mogą pozwolić na lepsze generowanie kodu na niektórych platformach, gdy używane są wartości mniejsze niż rozmiar natywnego słowa.
supercat
1
Gdyby te komentarze były w pierwszej kolejności odpowiedzią, zamiast rekomendacji i „wzywania imienia” bez podania przyczyny, nie skomentowałbym tego. ;) Chociaż nadal nie zgadzam się z „dziwactwem” tutaj, jest to po prostu definicja typu. Użyj odpowiedniego narzędzia do danego zadania i oczywiście poznaj to narzędzie. Typy niepodpisane są niewłaściwym narzędziem, gdy potrzebujesz relacji +/-. Istnieje powód, dla którego size_tjest niepodpisany i ptrdiff_tpodpisany.
Zabezpiecz
1
@ Bezpieczeństwo: jeśli chcemy reprezentować sekwencję bitów, typy bez znaku są świetne; Myślę, że się tam zgadzamy. Na niektórych małych mikrozpisywanych typach mogą być bardziej wydajne dla liczb. Są one również przydatne w przypadkach, w których delty reprezentują wielkości liczbowe, ale rzeczywiste wartości nie (np. Numery sekwencyjne TCP). Z drugiej strony, za każdym razem, gdy odejmujemy niepodpisane wartości, musimy martwić się o narożniki, nawet jeśli liczby są małe; takie matematyki z podpisanymi wartościami przedstawiają przypadki narożne tylko wtedy, gdy liczby są duże.
supercat
1

Używam niepodpisanych ints, aby mój kod i jego intencje były bardziej przejrzyste. Jedną rzeczą, którą robię, aby uchronić się przed nieoczekiwanymi niejawnymi konwersjami podczas wykonywania arytmetyki zarówno z typami podpisanymi, jak i niepodpisanymi, jest użycie niepodpisanego skrótu (zwykle 2 bajty) dla moich niepodpisanych zmiennych. Jest to skuteczne z kilku powodów:

  • Kiedy robisz arytmetykę ze swoimi niepodpisanymi krótkimi zmiennymi i literałami (które są typu int) lub zmiennymi typu int, zapewnia to, że niepodpisana zmienna zawsze będzie promowana do int przed oceną wyrażenia, ponieważ int zawsze ma wyższą rangę niż krótka . Pozwala to uniknąć nieoczekiwanego zachowania arytmetycznego z typami podpisanymi i niepodpisanymi, zakładając, że wynik wyrażenia pasuje oczywiście do podpisanego int.
  • W większości przypadków używane zmienne niepodpisane nie przekroczą maksymalnej wartości 2-bajtowego skrótu bez znaku (65 535)

Ogólna zasada jest taka, że ​​typ zmiennych bez znaku powinien mieć niższą rangę niż typ zmiennych ze znakiem, aby zapewnić awans do typu ze znakiem. Wtedy nie będziesz mieć żadnych nieoczekiwanych zachowań związanych z przepełnieniem. Oczywiście nie możesz tego zapewnić przez cały czas, ale (najczęściej) jest to możliwe.

Na przykład ostatnio miałem dla pętli coś takiego:

const unsigned short cuint = 5;
for(unsigned short i=0; i<10; ++i)
{
    if((i-2)%cuint == 0)
    {
       //Do something
    }
}

Dosłowne „2” jest typu int. Gdybym był liczbą całkowitą bez znaku zamiast skrótu bez znaku, wówczas w podwyrażeniu (i-2) 2 byłoby promowane do postaci bez znaku (ponieważ int ma wyższy priorytet niż znak int). Jeśli i = 0, to podwyrażenie jest równe (0u-2u) = pewna ogromna wartość z powodu przepełnienia. Ten sam pomysł z i = 1. Jednakże, ponieważ i jest skrótem bez znaku, jest promowany do tego samego typu, co literał „2”, który jest podpisany int i wszystko działa dobrze.

Dla większego bezpieczeństwa: w rzadkim przypadku, gdy implementowana architektura powoduje, że int ma 2 bajty, może to spowodować, że oba operandy w wyrażeniu arytmetycznym zostaną awansowane na int bez znaku w przypadku, gdy krótka zmienna bez znaku nie pasuje w podpisany 2-bajtowy int, którego ostatni ma maksymalną wartość 32 767 <65 535. (Zobacz https://stackoverflow.com/questions/17832815/c-implicit-conversion-signed-unsigned, aby uzyskać więcej informacji). Aby temu zapobiec, możesz po prostu dodać static_assert do swojego programu w następujący sposób:

static_assert(sizeof(int) == 4, "int must be 4 bytes");

i nie będzie się kompilował na architekturach, gdzie int ma 2 bajty.

AdmirałAdama
źródło