size_t lub int dla wymiarów, indeksu itp

15

W C ++ size_t(lub, bardziej poprawnie, T::size_typektóry jest „zwykle” size_t; tj. unsignedTyp) jest używany jako wartość zwracana dla size()argumentu argumentu operator[]itd. (Patrz std::vector, i in.)

Z drugiej strony języki .NET używają int(i opcjonalnie long) do tego samego celu; w rzeczywistości języki zgodne z CLS niewymagane do obsługi typów niepodpisanych .

Biorąc pod uwagę, że .NET jest nowszy niż C ++, coś mi mówi, że mogą występować problemy z używaniem unsigned intnawet do rzeczy, które „nie mogą” być ujemne, takie jak indeks tablicy lub długość. Czy podejście C ++ jest „historycznym artefaktem” dla kompatybilności wstecznej? Czy są prawdziwe i znaczące kompromisy między tymi dwoma podejściami?

Dlaczego to ma znaczenie? Cóż ... czego powinienem użyć dla nowej wielowymiarowej klasy w C ++; size_tczy int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};
.Аn
źródło
6
Warto zauważyć: w kilku miejscach w .NET Framework -1zwracane są funkcje zwracające indeks wskazujący „nie znaleziono” lub „poza zakresem”. Jest również zwracany z Compare()funkcji (implementujących IComparable). 32-bitowa liczba int jest uważana za typ dla liczby ogólnej, z tego, co mam nadzieję, są oczywiste powody.
Robert Harvey,

Odpowiedzi:

9

Biorąc pod uwagę, że .NET jest nowszy niż C ++, coś mi mówi, że mogą występować problemy z używaniem unsigned int, nawet dla rzeczy, które „nie mogą” być ujemne, takie jak indeks tablicy lub długość.

Tak. W przypadku niektórych rodzajów aplikacji, takich jak przetwarzanie obrazu lub przetwarzanie tablic, często konieczne jest uzyskanie dostępu do elementów w stosunku do bieżącej pozycji:

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

W tego typu aplikacjach nie można przeprowadzić sprawdzania zasięgu przy liczbach całkowitych bez znaku, bez dokładnego przemyślenia:

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Zamiast tego musisz zmienić ekspresję sprawdzania zasięgu. To jest główna różnica. Programiści muszą również pamiętać reguły konwersji liczb całkowitych. W razie wątpliwości ponownie przeczytaj http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

Wiele aplikacji nie musi używać bardzo dużych indeksów tablicowych, ale musi przeprowadzać kontrolę zasięgu. Co więcej, wielu programistów nie jest przeszkolonych do wykonywania gimnastyki polegającej na przestawianiu wyrażeń. Jedna utracona szansa otwiera drzwi do exploita.

C # jest rzeczywiście zaprojektowany dla aplikacji, które nie będą wymagały więcej niż 2 ^ 31 elementów na tablicę. Na przykład aplikacja arkusza kalkulacyjnego nie musi zajmować się tyloma wierszami, kolumnami lub komórkami. C # radzi sobie z górnym limitem, mając opcjonalną sprawdzoną arytmetykę, którą można włączyć dla bloku kodu ze słowem kluczowym bez bałaganu z opcjami kompilatora. Z tego powodu C # preferuje użycie podpisanej liczby całkowitej. Kiedy te decyzje są rozpatrywane w całości, ma to sens.

C ++ jest po prostu inny i trudniej jest uzyskać poprawny kod.

Jeśli chodzi o praktyczne znaczenie umożliwienia podpisanej arytmetyki usunięcia potencjalnego naruszenia „zasady najmniejszego zdziwienia”, przykładem może być OpenCV, który używa 32-bitowej liczby całkowitej ze znakiem dla wskaźnika elementu macierzy, rozmiaru tablicy, liczby kanałów pikseli itp. Obraz przetwarzanie jest przykładem domeny programowania, która mocno wykorzystuje względny indeks tablicowy. Niedopełniony niedopełnienie liczby całkowitej (zawinięty wynik ujemny) poważnie skomplikuje implementację algorytmu.

rwong
źródło
To jest dokładnie moja sytuacja; dzięki za konkretne przykłady. (Tak, wiem, ale może to być przydatne „wyższe władze” cytować.)
Ðаn
1
@ Dan: jeśli musisz coś zacytować, ten post byłby lepszy.
rwong
1
@Dan: John Regehr aktywnie bada ten problem w językach programowania. Zobacz blog.regehr.org/archives/1401
rwong
Istnieją sprzeczne opinie: gustedt.wordpress.com/2013/07/15/…
rwong
14

Ta odpowiedź naprawdę zależy od tego, kto będzie używał twojego kodu i jakie standardy chcą zobaczyć.

size_t jest liczbą całkowitą mającą cel:

Typ size_tjest zdefiniowaną implementacją typu liczba całkowita bez znaku, która jest wystarczająco duża, aby pomieścić rozmiar w bajtach dowolnego obiektu. (Specyfikacja C ++ 11 18.2.6)

Dlatego za każdym razem, gdy chcesz pracować z rozmiarem obiektów w bajtach, powinieneś użyć size_t. Teraz w wielu przypadkach nie używasz tych wymiarów / indeksów do zliczania bajtów, ale większość programistów decyduje się na użycie size_tich dla zachowania spójności.

Pamiętaj, że zawsze powinieneś używać, size_tjeśli twoja klasa ma wygląd i styl klasy STL. Wszystkie klasy STL w specyfikacji używają size_t. Jest to ważne dla kompilatora do typedef size_tbyć unsigned int, i to jest również ważne, aby była ona typedefed do unsigned long. Jeśli użyjesz intlub longbezpośrednio, ostatecznie spotkasz się z kompilatorami, w których osoba, która myśli, że twoja klasa postępowała zgodnie ze stylem STL, zostaje uwięziona, ponieważ nie postępujesz zgodnie ze standardem.

Jeśli chodzi o używanie podpisanych typów, jest kilka zalet:

  • Krótsze nazwy - pisanie jest naprawdę łatwe int, ale znacznie trudniej jest zaśmiecać kod unsigned int.
  • Jedna liczba całkowita dla każdego rozmiaru - Istnieje tylko jedna liczba 32-bitowa zgodna z CLS, czyli Int32. W C ++ są dwa ( int32_ti uint32_t). To może uprościć interoperacyjność API

Duża wada podpisanych typów jest oczywista: tracisz połowę swojej domeny. Podpisany numer nie może być liczony tak wysoko jak numer bez znaku. Kiedy pojawiło się C / C ++, było to bardzo ważne. Trzeba było mieć możliwość pełnego wykorzystania możliwości procesora, a do tego trzeba było używać liczb bez znaku.

W przypadku rodzajów aplikacji, na które ukierunkowane jest .NET, nie było tak silnej potrzeby indeksowania niepodpisanego pełnej domeny. Wiele celów takich liczb jest po prostu nieważnych w zarządzanym języku (przychodzi na myśl pula pamięci). Wraz z pojawieniem się platformy .NET 64-bitowe komputery były wyraźnie przyszłością. Jesteśmy daleko od potrzeby pełnego zakresu 64-bitowej liczby całkowitej, więc poświęcenie jednego bitu nie jest tak bolesne jak wcześniej. Jeśli naprawdę potrzebujesz 4 miliardów indeksów, po prostu przełącz się na używanie 64-bitowych liczb całkowitych. W najgorszym przypadku uruchamiasz go na 32-bitowej maszynie i jest on trochę powolny.

Uważam tę wymianę za wygodę. Jeśli akurat masz wystarczającą moc obliczeniową, że nie masz nic przeciwko marnowaniu części swojego indeksu, którego nigdy nie będziesz nigdy używać, to wygodnie jest po prostu wpisać intlub longodejść od niego. Jeśli okaże się, że naprawdę tego chciałeś, prawdopodobnie powinieneś zwrócić uwagę na podpis swoich numerów.

Cort Ammon
źródło
powiedzmy, że wdrożenie size()było return bar_ * baz_;; czy to nie stwarza teraz potencjalnego problemu z przepełnieniem liczb całkowitych (zawijaniem), którego nie miałbym, gdybym nie używał size_t?
Decаn
5
@ Dan Możesz budować takie przypadki, w których znaczenie miałyby niepodpisane inty, i w tych przypadkach najlepiej jest użyć pełnej wersji językowej, aby je rozwiązać. Muszę jednak powiedzieć, że byłoby ciekawą konstrukcją mieć klasę, w której bar_ * baz_może przepełnić liczbę całkowitą ze znakiem, ale nie liczbę całkowitą bez znaku. Ograniczając się do C ++, warto zauważyć, że w specyfikacji zdefiniowano przepełnienie niepodpisane, ale przepełnienie ze znakiem jest nieokreślonym zachowaniem, więc jeśli pożądana jest arytmetyka modulo niepodpisanych liczb całkowitych, zdecydowanie je wykorzystaj, ponieważ jest faktycznie zdefiniowane!
Cort Ammon
1
@Dan - jeślisize() przepełniła podpisaną mnożenie, jesteś w język UB ziemi. (i w fwrapvtrybie, patrz dalej :) Gdy wtedy , przy odrobinie odrobiny więcej, przepełniło się niepodpisane mnożenie, ty w krainie błędów użytkownika - zwrócisz fałszywy rozmiar. Więc nie sądzę, żeby niepodpisany kupował tutaj dużo.
Martin Ba
4

Myślę, że powyższa odpowiedź rwonga doskonale podkreśla problemy.

Dodam mój 002:

  • size_t, czyli rozmiar, który ...

    może przechowywać maksymalny rozmiar teoretycznie możliwego obiektu dowolnego typu (w tym tablicy).

    ... jest wymagany tylko w przypadku indeksów zakresów sizeof(type)==1, gdy mamy do czynienia z chartypami byte ( ). (Ale zauważamy, że może być mniejszy niż typ ptr :

  • Jako taki, xxx::size_typemoże być stosowany w 99,9% przypadków, nawet jeśli byłby to rozmiar wielkości ze znakiem. (porównaj ssize_t)
  • Fakt, że std::vectori przyjaciele wybralisize_t , bez znaku , rozmiar i indeksowanie, jest uważany przez niektórych za wadę projektową. Zgadzam się. (Poważnie, poświęć 5 minut i obejrzyj błyskawiczną rozmowę CppCon 2016: Jon Kalb „unsigned: A Guideline for Better Code” .)
  • Projektując API C ++ dzisiaj, jesteś w trudnym miejscu: Użyj size_t aby zachować spójność ze Standardową Biblioteką, lub użyj ( podpisanego ) intptr_tlub ssize_tdo łatwych i mniej podatnych na błędy obliczeń indeksowania.
  • Nie używaj int32 lub int64 - użyj, intptr_tjeśli chcesz podpisać się i chcesz rozmiar słowa maszynowego lub użyj ssize_t.

Aby bezpośrednio odpowiedzieć na pytanie, nie jest to całkowicie „artefakt historyczny”, ponieważ teoretyczny problem konieczności zajęcia się więcej niż połową („indeksowania” lub) przestrzeni adresowej musi być, aehm, w jakiś sposób rozwiązany w języku niskiego poziomu, takim jak C ++.

Z perspektywy czasu ja osobiście tak myślę jest to błąd projektowy, który Biblioteka Standardowa stosuje bez znaku w size_tcałym miejscu, nawet tam, gdzie nie reprezentuje surowego rozmiaru pamięci, ale pojemność wpisywanych danych, jak w przypadku kolekcji:

  • podane zasady promocji liczb całkowitych C ++ ->
  • typy bez znaku po prostu nie są dobrymi kandydatami na typy „semantyczne” dla czegoś takiego jak rozmiar, który jest semantycznie niepodpisany.

Będę powtarzać porady Jona tutaj:

  • Wybierz typy operacji, które obsługują (nie zakres wartości). (* 1)
  • Nie używaj niepodpisanych typów w swoim API. Ukrywa to błędy bez korzyści.
  • Nie używaj „bez znaku” dla ilości. (* 2)

(* 1) tj. Unsigned == maska ​​bitowa, nigdy nie wykonuj na nim obliczeń matematycznych (tutaj pojawia się pierwszy wyjątek - możesz potrzebować licznika, który się otacza - to musi być typ bez znaku)

(* 2) ilości oznaczające coś, co się liczy i / lub robi matematykę.

Martin Ba
źródło
Co masz na myśli przez „pełną dostępną płaską pamięć”? Ponadto, na pewno nie chcesz ssize_t, zdefiniowany jako podpisany wisiorek size_tzamiast intptr_t, który może przechowywać dowolny wskaźnik (niebędący członkiem), a zatem może być większy?
Deduplicator,
@Deduplicator - Cóż, myślę, że mogłem size_tnieco pomieszać definicję. Zobacz size_t vs. intptr i en.cppreference.com/w/cpp/types/size_t Nauczyłem się dzisiaj czegoś nowego. :-) Myślę, że reszta argumentów stoi, zobaczę, czy mogę naprawić użyte typy.
Martin Ba
0

Dodam tylko, że ze względu na wydajność zwykle używam size_t, aby upewnić się, że błędne obliczenia powodują niedopełnienie, co oznacza, że ​​obie kontrole zakresu (poniżej zera i powyżej size ()) można zmniejszyć do jednego:

przy użyciu podpisanego int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

przy użyciu unsigned int:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}
asger
źródło
1
Ty naprawdę chcesz, aby wyjaśnić, że jeden bardziej dokładnie.
Martin Ba
Aby uczynić odpowiedź bardziej użyteczną, być może możesz opisać, jak wygląda tablica liczb całkowitych lub porównanie przesunięć (podpisane i niepodpisane) w kodzie maszynowym od różnych dostawców kompilatora. Istnieje wiele internetowych kompilatorów C ++ i stron deasemblujących, które mogą wyświetlać odpowiedni skompilowany kod maszynowy dla danego kodu C ++ i flag kompilatora.
rwong
Próbowałem to trochę wyjaśnić.
asger