Pytanie Czy można bezpiecznie wziąć różnicę dwóch obiektów size_t?


Prowadzę dochodzenie w sprawie standardu używanego przez mój zespół size_t vs int (lub longitp.). Największą wadą, którą zauważyłem, jest to, że wzięcie różnicy dwóch obiektów size_t może spowodować problemy (nie jestem pewien konkretnych problemów - być może coś nie zostało uzupełnione o 2s, a podpisane / niepodpisane gniewnie kompilatora). Napisałem szybki program w C ++ przy użyciu kompilatora V120 VS2013, który pozwolił mi wykonać następujące czynności:

#include <iostream>

main()
{
    size_t a = 10;
    size_t b = 100;
    int result = a - b;
}

Program zaowocował: -90, co prawda, denerwuje mnie niedopasowanie typu, podpisane / niepodpisane problemy, czy po prostu niezdefiniowane zachowanie, jeśli size_t stanie się użytym w skomplikowanej matematyce.

Moje pytanie brzmi: czy można bezpiecznie wykonywać matematykę z obiektami size_t, szczególnie biorąc pod uwagę różnicę? Rozważam użycie size_t jako standardu dla takich rzeczy jak indeksy. Widziałem tu kilka ciekawych postów na ten temat, ale nie rozwiązują problemu matematycznego (lub go przegapiłem).

Jaki typ odjąć 2 size_t's?

typedef dla podpisanego typu, który może zawierać size_t?


14
2018-01-06 21:04


pochodzenie


Jeśli odejmowanie daje liczbę ujemną, będzie się zawijać według komplement dla dwóch osób - CoryKramer
Tak jak to masz, nie, to nie jest bezpieczne. Ale nie z powodu samej matematyki. Odlewasz size_t wynik (który prawdopodobnie będzie unsigned long) do a int (podpis), który prawdopodobnie będzie mniejszym typem danych. - Mad Physicist
Odpowiedni typ różnicy size_t to ptrdiff_t, a nie int - Dieter Lücking
@ DieterLücking faktycznie, odpowiedni typ jest nadal size_tnie oznacza to, że pomaga OP. - Mad Physicist
@MadPhysicist - re: "Rzucacie ...", tam nie ma obsady. Jest jeden konwersja. Obsada to coś, co piszesz w swoim kodzie źródłowym, aby powiedzieć kompilatorowi, aby zrobił konwersję. - Pete Becker


Odpowiedzi:


Nie można zagwarantować, że będzie działać w sposób przenośny, ale nie będzie również UB. Kod musi działać bez błędu, ale wynik int wartość to zdefiniowana implementacja. Tak długo, jak pracujesz na platformach, które gwarantują pożądane zachowanie, jest to w porządku (o ile różnica może być reprezentowana przez int oczywiście), w przeciwnym razie wystarczy użyć podpisanych typów wszędzie (patrz ostatni akapit).

Odejmowanie dwóch std::size_ts przyniesie nowy std::size_t a jego wartość zostanie określona przez owijanie. W twoim przykładzie, zakładając 64-bitowy size_t, a - b będzie równa 18446744073709551526. To nie pasuje do (powszechnie używany 32-bitowy) int, więc zdefiniowana wartość implementacji jest przypisana do result.

Szczerze mówiąc, nie polecałbym używać liczb całkowitych bez znaku dla niczego poza odrobiną magii. Kilku członków standardowej komisji zgadza się ze mną: https://channel9.msdn.com/Events/GoingNative/2013/Interactive-Panel-Ask-Us-Anything 9:50, 42:40, 1:02:50

Zasada kciuka (parafrazując Chandler'a Carruth'a z powyższego wideo): Jeśli mógłbyś to zliczyć samodzielnie, użyj int, w przeciwnym razie użyj std::int64_t.


Chyba że jego stopień konwersji jest mniejszy niż int, np. gdyby std::size_t jest unsigned short. W takim przypadku wynikiem jest int i wszystko będzie działać dobrze (chyba, że int nie jest szerszy niż short). jednak

  1. Nie znam żadnej platformy, która to robi.
  2. Byłoby to nadal specyficzne dla platformy, patrz akapit pierwszy.

14
2018-01-06 21:12



Istnieje wiele platform, w których sizeof(int)==sizeof(short) i sizeof(int)==sizeof(size_t) (wszystkie 16 bitów). To prawie na każdym mikrokontrolercie 8 i 16 bitowym (AVR, PIC10-18, MSP430, ...) - 12431234123412341234123


Jeśli nie używasz size_t, masz przechlapane: size_t jest jedynym typem, który ma być stosowany w przypadku rozmiarów pamięci, i który w konsekwencji gwarantuje, że zawsze będzie wystarczająco duży do tego celu. (uintptr_t jest dość podobny, ale nie jest pierwszym takim typem, ani nie jest używany w standardowych bibliotekach, ani nie jest dostępny bez dołączania stdint.h.) Jeśli używasz int, możesz uzyskać niezdefiniowane zachowanie, gdy twoje przydziały przekroczą 2GiB przestrzeni adresowej (lub 32kiB, jeśli jesteś na platformie, gdzie intto tylko 16 bitów!), nawet jeśli maszyna ma więcej pamięci i wykonujesz ją w trybie 64-bitowym.

Jeśli potrzebujesz różnicę size_t które mogą stać się ujemne, użyj podpisanego wariantu ssize_t.


5
2018-01-06 21:21



"użyj podpisanego wariantu ssize_t"... jak? Rzucasz przed lub po odejmowaniu? A co z utraconym zasięgiem? - Ben Voigt
@BenVoigt Utracony zasięg z ssize_t jest jeden, utracony zakres int jest często 32-bitowy, może nawet być 48-bitowy (jeśli int jest 16 bitów i size_t jest 64-bitowe). Ergo: Używanie ssize_t jest zdecydowanie lepszy niż użycie int. - cmaster
@BenVoigt Nie ma znaczenia, kiedy wykonasz obsadę: Jeśli doznasz przepełnienia, i tak jesteś skręcony, zarówno int i ssize_t. Wszystkie testy poprawności należy wykonać przed obliczeniem różnicy. - cmaster
Gdyby uintptr_t nie może pomieścić żadnego rozmiaru pamięci size_t mogę wytrzymać, zjem moje szorty. - Kaz
@Kaz Dobra uwaga. Usunąłem słowo only i zastąpił go notatką o uintptr_t. Mam nadzieję, że Cię uszczęśliwia :-) - cmaster


The size_t typ jest niepodpisany. Odejmowanie dowolnych dwóch size_t wartości to zdefiniowane - zachowanie

Jednak, po pierwsze, wynik jest definiowany przez implementację, jeśli większa wartość jest odejmowana od mniejszej. Wynikiem jest wartość matematyczna zredukowana do najmniejszego dodatniego modulo pozostałości SIZE_T_MAX + 1. Na przykład, jeśli największa wartość size_t to 65535, a wynik odejmowania dwóch size_t wartości to -3, wtedy wynik będzie 65536 - 3 = 65533. Na innym kompilatorze lub maszynie z innym size_t, wartość numeryczna będzie inna.

Po drugie, a size_t wartość może być poza zakresem typu int. W takim przypadku otrzymujemy drugi wynik zdefiniowany w ramach implementacji wynikający z wymuszonej konwersji. W tej sytuacji każde zachowanie może mieć zastosowanie; to musi być udokumentowane przez implementację, a konwersja nie może zawieść. Na przykład wynik może zostać zaciśnięty w int zasięg, produkcja INT_MAX. Typowym zachowaniem na maszynach uzupełniających dwóch (praktycznie wszystkich) w konwersji szerszych (lub równych) typów bez podpisu do węższych typów podpisów jest proste obcinanie bitów: wystarczająca liczba bitów pochodzi z wartości bez znaku, aby wypełnić podpisaną wartość, w tym jej znak kawałek.

Ze względu na sposób, w jaki działa dopełnienie dwójki, jeśli oryginalny, arytmetycznie poprawny wynik sam w sobie pasuje int, wtedy konwersja da taki wynik.

Na przykład załóżmy, że odejmujemy parę 64-bitową size_t wartości na maszynie z dopełnieniem dwójkowym dają abstrakcyjną wartość arytmetyczną -3, która staje się wartością dodatnią 0xFFFFFFFFFFFFFFFD. Kiedy jest to wymuszone na 32 bity int, a często spotykanym w wielu kompilatorach dla maszyn z dodatkami dwóch jest to, że niższe 32 bity są traktowane jako obraz wynikowego int: 0xFFFFFFFD. I, oczywiście, jest to tylko wartość -3 w 32 bitach.

Wynik jest taki twój kod jest de facto całkiem przenośny ponieważ praktycznie wszystkie maszyny głównego nurtu są uzupełnieniem dwóch z regułami konwersji opartymi na rozszerzeniu znaku i obcinaniu bitów, w tym między znakiem i nie podpisanym.

Z wyjątkiem tego, że rozszerzenie znaku nie występuje, gdy wartość jest poszerzona podczas konwersji z podpisu bez znaku. Zatem jednym z problemów jest rzadka sytuacja int jest szerszy niż size_t. Jeśli 16 bit size_t wynik to 65533, ponieważ 4 jest odejmowane od 1, nie spowoduje to -3 po przekonwertowaniu do 32 bitów int; wyprodukuje 65533!


4
2018-01-06 21:36



Być może masz na myśli "zależny od implementacji", a nie "zdefiniowany przez implementację"? Ten ostatni termin ma formalne znaczenie przypisane przez standard C ++. - Ben Voigt
W twoim ostatnim paragrafie zapomniałeś wziąć pod uwagę całkowe promocje, które są wykonywane przed odejmowaniem. - Ben Voigt
@BenVoigt Right; ale to będzie rzadkie. Jak powszechne jest to size_t odpowiadać unsigned short lub unsigned char? Wciąż ważny punkt. Jeśli chodzi o "wynik zdefiniowany przez implementację", to użycie jest we mnie zakorzenione z ISO C. Wszystko w tej odpowiedzi dotyczy C i C ++. - Kaz