Używanie liczb całkowitych bez znaku w C i C ++

23

Mam bardzo proste pytanie, które mnie zaskakuje przez długi czas. Mam do czynienia z sieciami i bazami danych, więc wiele danych, którymi się zajmuję, to liczniki 32-bitowe i 64-bitowe (niepodpisane), 32-bitowe i 64-bitowe identyfikatory (również nie mają znaczącego odwzorowania znaku). Praktycznie nigdy nie mam do czynienia z prawdziwymi słowami, które można wyrazić jako liczby ujemne.

Ja i moi współpracownicy rutynowo używamy niepodpisanych typów, takich jak uint32_ti uint64_tdo tych spraw, a ponieważ tak się często zdarza, używamy ich również do indeksów tablic i innych typowych liczb całkowitych.

Jednocześnie różne przewodniki kodowania, które czytam (np. Google), zniechęcają do używania niepodpisanych typów liczb całkowitych i, o ile wiem, ani Java, ani Scala nie mają typów liczb całkowitych bez znaku.

Nie mogłem więc zorientować się, co należy zrobić: używanie podpisanych wartości w naszym środowisku byłoby bardzo niewygodne, a jednocześnie kodowanie przewodników, aby nalegać na zrobienie tego dokładnie.

zzz777
źródło

Odpowiedzi:

31

Są na to dwie szkoły myślenia i żadna z nich się nigdy nie zgodzi.

Pierwszy argumentuje, że istnieją pewne koncepcje, które z natury są niepodpisane - takie jak indeksy tablic. Nie ma sensu używać podpisanych liczb dla tych, ponieważ może to prowadzić do błędów. Może także nakładać niepotrzebne ograniczenia na rzeczy - tablica, która używa 32-bitowych indeksów ze znakiem, może uzyskać dostęp tylko do 2 miliardów pozycji, a przejście na 32-bitowe liczby bez znaku pozwala na 4 miliardy pozycji.

Drugi argumentuje, że w każdym programie, który używa liczb niepodpisanych, prędzej czy później skończysz mieszaną arytmetyką bez podpisu. Może to dawać dziwne i nieoczekiwane wyniki: rzutowanie dużej wartości bez znaku na podpis daje liczbę ujemną, a odwrotnie rzutowanie liczby ujemnej na bez znaku daje dużą wartość dodatnią. To może być dużym źródłem błędów.

Simon B.
źródło
8
Mieszane problemy arytmetyczne ze znakiem i bez znaku są wykrywane przez kompilator; po prostu zachowaj swoją kompilację bez ostrzeżeń (z wystarczająco wysokim poziomem ostrzegania). Poza tym intjest krótszy do
wpisania
7
Spowiedź: Jestem z drugą szkołą myślenia i choć rozumiem rozważania dotyczące typów niepodpisanych: intwystarczy dla indeksów tablicowych 99,99% razy. Podpisane - niepodpisane problemy arytmetyczne są znacznie częstsze, dlatego mają pierwszeństwo w zakresie tego, czego należy unikać. Tak, kompilatory ostrzegają o tym, ale ile ostrzeżeń pojawia się podczas kompilacji dużego projektu? Ignorowanie ostrzeżeń jest niebezpieczne i złe, ale w prawdziwym świecie ...
Elias Van Ootegem
11
+1 do odpowiedzi. Uwaga : Tępe opinie w przód : 1: Moja odpowiedź na drugą szkołę myślenia brzmi: założę się, że każdy, kto uzyska nieoczekiwane wyniki z niepodpisanych integralnych typów w C, będzie miał niezdefiniowane zachowanie (a nie czysto akademickie) w ich nietrywialne programy C, które wykorzystują podpisane typy całkowe. Jeśli nie znasz wystarczająco C, aby myśleć, że lepszymi typami są niepodpisane typy , radzę unikać C. 2: Jest dokładnie jeden poprawny typ dla indeksów tablic i rozmiarów w C, i to size_tchyba, że ​​jest to przypadek specjalny nie bez powodu.
mtraceur
5
Wpadasz w kłopoty bez mieszanego podpisu. Wystarczy obliczyć unsigned int minus unsigned int.
gnasher729
4
Nie mam z tobą problemu, Simon, tylko z pierwszą szkołą myślenia, która twierdzi, że „istnieją pewne koncepcje, które z natury są niepodpisane - takie jak indeksy tablic”. w szczególności: „Jest dokładnie jeden poprawny typ dla indeksów tablicowych ... w C”, Bullshit! . My DSPers cały czas używamy indeksów ujemnych. szczególnie przy parzystych lub nieparzystych symetrycznych odpowiedziach impulsowych, które nie są przyczynowe. i dla matematyki LUT. Jestem w drugiej szkole myślenia, ale myślę, że dobrze jest mieć zarówno liczby całkowite ze znakiem, jak i bez znaku w C i C ++.
Robert Bristol-Johnson
21

Po pierwsze, wytyczne kodowania Google C ++ nie są zbyt dobre do naśladowania: unika takich rzeczy jak wyjątki, ulepszenia itp., Które są podstawowymi elementami współczesnego C ++. Po drugie, tylko dlatego, że pewne wytyczne działają dla firmy X, nie oznacza, że ​​będą dla ciebie odpowiednie. Nadal używałbym typów niepodpisanych, ponieważ bardzo ich potrzebujesz.

Przyzwoitą zasadą dla C ++ jest: wolę, intchyba że masz dobry powód, aby użyć czegoś innego.

bstamour
źródło
8
Wcale nie o to mi chodzi. Konstruktory służą do ustanawiania niezmienników, a ponieważ nie są funkcjami, nie mogą po prostu, return falsejeśli niezmiennik ten nie zostanie ustalony. Możesz więc albo rozdzielić rzeczy i użyć funkcji init dla swoich obiektów, albo możesz rzucić std::runtime_error, pozwolić, aby nastąpiło odwinięcie stosu, i pozwolić wszystkim twoim obiektom RAII na automatyczne czyszczenie się, a Ty programista może obsłużyć wyjątek, w którym jest to wygodne dla aby to zrobić.
bstamour
5
Nie rozumiem, jak zmienia się rodzaj aplikacji. Za każdym razem, gdy wywołujesz konstruktor na obiekcie, ustanawiasz niezmiennik z parametrami. Jeśli tego niezmiennika nie można spełnić, musisz zasygnalizować błąd, w przeciwnym razie program nie będzie w dobrym stanie. Ponieważ konstruktory nie mogą zwrócić flagi, zgłoszenie wyjątku jest naturalną opcją. Podaj solidny argument, dlaczego aplikacja biznesowa nie skorzysta z takiego stylu kodowania.
bstamour
8
Bardzo wątpię, że połowa wszystkich programistów C ++ nie jest w stanie właściwie używać wyjątków. Ale w każdym razie, jeśli uważasz, że twoi współpracownicy nie są w stanie napisać nowoczesnego C ++, to zdecydowanie trzymaj się z dala od współczesnego C ++.
bstamour
6
@ zzz777 Nie używasz wyjątków? Czy konstruktory prywatne są pakowane przez funkcje publicznej fabryki, które wychwytują wyjątki i robią co - zwracają nullptr? zwrócić obiekt „domyślny” (cokolwiek to może znaczyć)? Niczego nie rozwiązałeś - ukryłeś problem pod dywanem i mam nadzieję, że nikt się nie dowie.
Mael
5
@ zzz777 Jeśli i tak zamierzasz rozbić to okno, dlaczego przejmujesz się, że dzieje się tak z powodu wyjątku lub signal(6)? Jeśli użyjesz wyjątku, 50% programistów, którzy wiedzą, jak sobie z nimi poradzić, może napisać dobry kod, a resztę mogą ponieść ich rówieśnicy.
IllusiveBrian
6

W innych odpowiedziach brakuje przykładów ze świata rzeczywistego, więc dodam jeden. Jednym z powodów, dla których (osobiście) staram się unikać typów niepodpisanych.

Rozważ użycie standardowego rozmiaru_t jako indeksu tablicy:

for (size_t i = 0; i < n; ++i)
    // do something here;

Ok, zupełnie normalne. Następnie zastanów się, czy z jakiegoś powodu postanowiliśmy zmienić kierunek pętli:

for (size_t i = n - 1; i >= 0; --i)
    // do something here;

A teraz to nie działa. Gdybyśmy zastosowali intjako iterator, nie byłoby problemu. Widziałem taki błąd dwa razy w ciągu ostatnich dwóch lat. Kiedyś stało się to w produkcji i było trudne do debugowania.

Innym powodem dla mnie są irytujące ostrzeżenia, które powodują, że za każdym razem piszesz coś takiego :

int n = 123;  // for some reason n is signed
...
for (size_t i = 0; i < size_t(n); ++i)

To są drobne rzeczy, ale się sumują. Wydaje mi się, że kod jest czystszy, jeśli wszędzie używane są tylko liczby całkowite ze znakiem.

Edycja: Oczywiście, przykłady wyglądają głupio, ale widziałem ludzi popełniających ten błąd. Jeśli istnieje taki łatwy sposób, aby tego uniknąć, dlaczego go nie użyć?

Kiedy kompiluję następujący fragment kodu z VS2015 lub GCC, nie widzę ostrzeżeń z domyślnymi ustawieniami ostrzeżeń (nawet z -Wall dla GCC). Musisz poprosić o -Wextra, aby otrzymać ostrzeżenie o tym w GCC. Jest to jeden z powodów, dla których powinieneś zawsze kompilować z Wall i Wextra (i używać analizatora statycznego), ale w wielu prawdziwych projektach ludzie tego nie robią.

#include <vector>
#include <iostream>


void unsignedTest()
{
    std::vector<int> v{ 1, 2 };

    for (int i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;

    for (size_t i = v.size() - 1; i >= 0; --i)
        std::cout << v[i] << std::endl;
}

int main()
{
    unsignedTest();
    return 0;
}
Aleksei Petrenko
źródło
Możesz zrobić to jeszcze bardziej źle z podpisanymi typami ... A twój przykładowy kod jest tak przerażający i rażąco zły, że każdy porządny kompilator ostrzeże cię, jeśli poprosisz o ostrzeżenia.
Deduplicator,
1
W przeszłości uciekłem się do takich okropności, for (size_t i = n - 1; i < n; --i)aby wszystko działało poprawnie.
Simon B
2
Mówiąc o pętlach for z size_todwrotnością, istnieje wytyczna kodowania w stylufor (size_t revind = 0u; revind < n; ++revind) { size_t ind = n - 1u - revind; func(ind); }
rwong
2
@rwong Omg, to jest brzydkie. Dlaczego nie po prostu użyć int? :)
Aleksei Petrenko
1
@AlexeyPetrenko - należy pamiętać, że ani obecne standardy C ani C ++ nie gwarantują, że intjest wystarczająco duży, aby pomieścić wszystkie prawidłowe wartości size_t. W szczególności intmoże zezwalać na liczby tylko do 2 ^ 15-1 i zwykle robi to w systemach, które mają limity alokacji pamięci 2 ^ 16 (lub w niektórych przypadkach nawet wyższe). longmoże być bezpieczniejszym zakładem, choć nadal nie gwarantuje, że zadziała. Tylko size_tgwarantuje się, że działa na wszystkich platformach i we wszystkich przypadkach.
Jules
4
for (size_t i = v.size() - 1; i >= 0; --i)
   std::cout << v[i] << std::endl;

Problem polega na tym, że napisałeś pętlę w nieuczciwy sposób, co prowadzi do błędnego zachowania. Konstrukcja pętli przypomina nauczenie początkujących dla typów podpisanych (co jest poprawne i poprawne), ale po prostu nie pasuje do niepodpisanych wartości. Ale nie może to służyć jako argument przeciwny używaniu typów niepodpisanych, zadaniem tutaj jest po prostu poprawne wykonanie pętli. Można to łatwo naprawić, aby niezawodnie działało dla typów niepodpisanych, takich jak:

for (size_t i = v.size(); i-- > 0; )
    std::cout << v[i] << std::endl;

Ta zmiana po prostu odwraca sekwencję porównywania i operacji dekrementacji i jest moim zdaniem najbardziej skutecznym, niezakłócającym, czystym i najkrótszym sposobem na obsługę niepodpisanych liczników w pętlach wstecznych. Zrobiłbyś to samo (intuicyjnie), używając pętli while:

size_t i = v.size();
while (i > 0)
{
    --i;
    std::cout << v[i] << std::endl;
}

Niedomiar nie może wystąpić, przypadek pustego pojemnika jest domyślnie objęty, tak jak w dobrze znanym wariancie dla podpisanej pętli licznika, a korpus pętli może pozostać niezmieniony w porównaniu do podpisanego licznika lub pętli przekazywania. Musisz tylko przyzwyczaić się do nieco dziwnie wyglądającej konstrukcji pętli. Ale po tym, jak zobaczyłeś to kilkanaście razy, nie ma już nic niezrozumiałego.

Miałbym szczęście, gdyby kursy dla początkujących pokazywały nie tylko prawidłową pętlę dla typów podpisanych, ale także dla typów niepodpisanych. Pozwoliłoby to uniknąć kilku błędów, które należy winić IMHO nieświadomym deweloperom zamiast obwiniać niepodpisany typ.

HTH

Don Pedro
źródło
1

Istnieją niepodpisane liczby całkowite z jakiegoś powodu.

Rozważmy na przykład przekazywanie danych jako pojedynczych bajtów, np. W pakiecie sieciowym lub buforze plików. Czasami możesz spotkać takie bestie, jak 24-bitowe liczby całkowite. Łatwo przesunięte bitowo z trzech 8-bitowych liczb całkowitych bez znaku, nie tak łatwo z 8-bitowymi liczbami całkowitymi ze znakiem.

Lub pomyśl o algorytmach wykorzystujących tabele odnośników. Jeśli znak jest 8-bitową liczbą całkowitą bez znaku, możesz indeksować tabelę wyszukiwania według wartości znaku. Co jednak robisz, jeśli język programowania nie obsługuje liczb całkowitych bez znaku? Miałbyś ujemne indeksy do tablicy. Cóż, myślę, że możesz użyć czegoś takiego, charval + 128ale to po prostu brzydkie.

W rzeczywistości wiele formatów plików używa liczb całkowitych bez znaku, a jeśli język programowania aplikacji nie obsługuje liczb całkowitych bez znaku, może to stanowić problem.

Następnie rozważ numery sekwencyjne TCP. Jeśli napiszesz kod przetwarzania TCP, na pewno będziesz chciał użyć liczb całkowitych bez znaku.

Czasami wydajność ma tak duże znaczenie, że naprawdę potrzebujesz dodatkowej liczby liczb całkowitych bez znaku. Rozważmy na przykład urządzenia IoT dostarczane w milionach. Można wówczas uzasadnić wiele zasobów programistycznych, które można przeznaczyć na mikrooptymalizacje.

Argumentowałbym, że uzasadnienie unikania używania liczb całkowitych bez znaku (arytmetyka znaków mieszanych, porównania znaków mieszanych) można pokonać kompilatorem z odpowiednimi ostrzeżeniami. Takie ostrzeżenia zwykle nie są domyślnie włączone, ale patrz np. -WextraLub osobno -Wsign-compare(automatyczne włączanie w C przez -Wextra, chociaż nie sądzę, że jest włączane automatycznie w C ++) i -Wsign-conversion.

Niemniej jednak w razie wątpliwości użyj podpisanego typu. Wiele razy jest to dobry wybór. I włącz te ostrzeżenia kompilatora!

juhist
źródło
0

Istnieje wiele przypadków, w których liczby całkowite nie reprezentują liczb, ale na przykład maska ​​bitowa, identyfikator itp. Zasadniczo przypadki, w których dodanie 1 do liczby całkowitej nie ma żadnego znaczącego wyniku. W takich przypadkach użyj niepodpisanego.

Istnieje wiele przypadków, w których wykonujesz arytmetykę liczbami całkowitymi. W takich przypadkach należy użyć liczb całkowitych ze znakiem, aby uniknąć niewłaściwego zachowania w pobliżu zera. Zobacz wiele przykładów z pętlami, w których uruchomienie pętli do zera albo używa bardzo nieintuicyjnego kodu, albo jest zepsute z powodu użycia niepodpisanych liczb. Istnieje argument „ale wskaźniki nigdy nie są ujemne” - jasne, ale na przykład różnice wskaźników są ujemne.

W bardzo rzadkim przypadku, gdy indeksy przekraczają 2 ^ 31, ale nie 2 ^ 32, nie używasz liczb całkowitych bez znaku, używasz liczb całkowitych 64-bitowych.

Wreszcie ładna pułapka: w pętli „dla (i = 0; i <n; ++ i) a [i] ...” jeśli i jest niepodpisany 32-bitowy, a pamięć przekracza 32-bitowe adresy, kompilator nie może zoptymalizować dostęp do [i] poprzez zwiększenie wskaźnika, ponieważ przy i = 2 ^ 32 - 1 zawijam się. Nawet gdy n nigdy nie jest tak duży. Użycie podpisanych liczb całkowitych pozwala tego uniknąć.

gnasher729
źródło
-5

Wreszcie znalazłem naprawdę dobrą odpowiedź: „Bezpieczne programowanie książki kucharskiej” J.Viega i M.Messier ( http://shop.oreilly.com/product/9780596003944.do )

Problemy bezpieczeństwa z podpisanymi liczbami całkowitymi:

  1. Jeśli funkcja wymaga dodatniego parametru, łatwo zapomnieć o sprawdzeniu dolnego zakresu.
  2. Nieintuicyjny wzór bitowy z konwersji ujemnych liczb całkowitych.
  3. Nieintuicyjny wzór bitowy powstały w wyniku operacji przesunięcia w prawo ujemnej liczby całkowitej.

Występują problemy z podpisanymi konwersjami <-> niepodpisanymi, więc nie zaleca się używania miksu.

zzz777
źródło
1
Dlaczego to dobra odpowiedź? Co to jest przepis 3.5? Co to mówi o przepełnieniu liczb całkowitych itp.?
Baldrickk,
Z mojego praktycznego doświadczenia Jest to bardzo dobra książka z cennymi poradami w innych aspektach, których próbowałem i jest dość twarda w tym zaleceniu. W porównaniu z niebezpieczeństwem przepełnienia liczb całkowitych na tablicach dłuższych niż 4G wydaje się dość słabe. Jeśli będę musiał radzić sobie z tak dużymi tablicami, mój program będzie miał wiele dostrajania, aby uniknąć kar za wydajność.
zzz777
1
nie chodzi o to, czy książka jest dobra. Twoja odpowiedź nie zawiera uzasadnienia dla zastosowania przepisu i nie każdy będzie miał kopię książki, aby ją sprawdzić. Spójrz na przykłady, jak napisać dobrą odpowiedź
Baldrickk,
Do Twojej wiadomości właśnie dowiedziałem się o innym przyczynie używania liczb całkowitych bez znaku: można łatwo wykryć przepełnienie: youtube.com/…
zzz777