Czy „długi” zakaz ma sens?

109

W dzisiejszym cross-platform C ++ (lub C) Świat możemy mieć :

Data model  | short |   int |   long | long long | pointers/size_t  | Sample operating systems
... 
LLP64/IL32P64   16      32      32     64           64                Microsoft Windows (x86-64 and IA-64)
LP64/I32LP64    16      32      64     64           64                Most Unix and Unix-like systems, e.g. Solaris, Linux, BSD, and OS X; z/OS
...

Oznacza to dzisiaj, że dla każdej „wspólnej” (podpisanej) liczby całkowitej intwystarczy i może być nadal używana jako domyślny typ liczby całkowitej podczas pisania kodu aplikacji C ++. Będzie również - dla obecnych praktycznych celów - mieć jednolity rozmiar na różnych platformach.

Jeśli przypadek użycia wymaga co najmniej 64 bitów, możemy dzisiaj użyć long long, chociaż możliwe, że użyjemy jednego z typów określających bitowość lub __int64typ może mieć większy sens.

Pozostaje to longw środku i rozważamy całkowity zakaz korzystania longz naszego kodu aplikacji .

Czy miałoby to sens , czy jest uzasadnione użycie longnowoczesnego kodu C ++ (lub C), który musi działać na różnych platformach? (platforma to komputer stacjonarny, urządzenia mobilne, ale nie takie rzeczy jak mikrokontrolery, procesory DSP itp.)


Prawdopodobnie interesujące linki w tle:

Martin Ba
źródło
14
Jak poradzisz sobie z połączeniami do bibliotek, które używają długo?
Ángel
14
longto jedyny sposób na zagwarantowanie 32 bitów. intmoże mieć 16 bitów, więc w niektórych aplikacjach to nie wystarczy. Tak, intczasami jest 16 bitów w nowoczesnych kompilatorach. Tak, ludzie piszą oprogramowanie na mikrokontrolerach. Twierdziłbym, że więcej ludzi pisze oprogramowanie, które ma więcej użytkowników na mikrokontrolerach niż na PC, wraz z rozwojem urządzeń iPhone i Android, nie wspominając o rozwoju Arduinos itp.
Slebetman
53
Dlaczego nie banować znaków char, short, int, long i long long i używać typów [u] intXX_t?
immibis
7
@slebetman Kopałem nieco głębiej, wydaje się, że wymóg jest nadal na miejscu, chociaż ukryty w §3.9.1.3, gdzie standard C ++ stwierdza: „Podpisane i niepodpisane typy liczb całkowitych powinny spełniać ograniczenia podane w standardzie C, sekcja 5.2. 4.2.1. ” A w standardzie C §5.2.4.2.1 określa minimalny zakres, dokładnie tak, jak napisałeś. Miałeś absolutną rację. :) Najwyraźniej posiadanie kopii standardu C ++ to za mało, trzeba też znaleźć kopię standardu C.
Tommy Andersen
11
Brakuje Ci świata DOSBox / Turbo C ++, w którym intwciąż jest bardzo 16 bitów. Nienawidzę tego mówić, ale jeśli zamierzasz pisać o „dzisiejszym wieloplatformowym świecie”, nie możesz zignorować całego subkontynentu indyjskiego.
Wyścigi lekkości na orbicie

Odpowiedzi:

17

Jedynym powodem, dla którego użyłbym longdzisiaj, jest wywołanie lub wdrożenie zewnętrznego interfejsu, który go używa.

Jak powiedziałeś w swoim poście, krótkie i int mają dość stabilną charakterystykę na wszystkich głównych platformach komputerów stacjonarnych / serwerów / urządzeń mobilnych i nie widzę powodu, aby zmienić to w dającej się przewidzieć przyszłości. Więc nie widzę powodu, by w ogóle ich unikać.

longz drugiej strony jest bałagan. Na wszystkich systemach 32-bitowych jestem tego świadomy, że miał następujące cechy.

  1. Miał dokładnie 32 bity.
  2. Miał ten sam rozmiar co adres pamięci.
  3. Miał ten sam rozmiar co największa jednostka danych, którą można było przechowywać w zwykłym rejestrze i pracować z jedną instrukcją.

Duże ilości kodu zostały napisane w oparciu o jedną lub więcej z tych cech. Jednak po przejściu na wersję 64-bitową nie było możliwe zachowanie wszystkich z nich. Platformy uniksowe poszły na LP64, który zachował cechy 2 i 3 kosztem cechy 1. Win64 zdecydował się na LLP64, który zachował charakterystykę 1 kosztem cech 2 i 3. W rezultacie nie można już polegać na żadnej z tych cech i że IMO pozostawia niewiele powodów do użycia long.

Jeśli chcesz mieć rozmiar dokładnie 32-bitowy, powinieneś użyć int32_t.

Jeśli chcesz mieć czcionkę tego samego rozmiaru co wskaźnik, powinieneś użyć intptr_t(lub lepiej uintptr_t).

Jeśli chcesz typu, który jest największym przedmiotem, nad którym można pracować w jednym rejestrze / instrukcji, niestety nie sądzę, że standard go przewiduje. size_tpowinien być odpowiedni na większości popularnych platform, ale nie byłby na x32 .


PS

Nie zawracałbym sobie głowy typami „szybkimi” lub „najmniej”. Typy „najmniejsze” mają znaczenie tylko wtedy, gdy zależy Ci na przenośności, aby naprawdę zaciemnić architekturę CHAR_BIT != 8. Rozmiar „szybkich” typów w praktyce wydaje się dość arbitralny. Wydaje się, że Linux sprawia, że ​​są co najmniej tego samego rozmiaru co wskaźnik, co jest głupie na 64-bitowych platformach z szybką obsługą 32-bitową, taką jak x86-64 i arm64. IIRC iOS czyni je tak małymi, jak to możliwe. Nie jestem pewien, co robią inne systemy.


PPS

Jednym z powodów użycia unsigned long(ale nie prostego long) jest to, że zachowuje się modulo. Niestety ze względu na zepsute zasady promocji C typy niepodpisane mniejsze niż intnie zachowują się modulo.

Na wszystkich głównych platformach dzisiaj uint32_tjest tego samego rozmiaru lub większy niż int, a zatem ma zachowanie modulo. Jednak były historycznie i teoretycznie mogą istnieć na przyszłych platformach, gdzie intjest 64-bitowy, a zatem uint32_tnie ma zachowania modulo.

Osobiście powiedziałbym, że lepiej jest wpaść w nawyk wymuszania zachowania modulo, używając „1u *” lub „0u +” na początku twoich równań, ponieważ będzie to działać dla dowolnego rozmiaru typu bez znaku.

Peter Green
źródło
1
Wszystkie typy „określonego rozmiaru” byłyby znacznie bardziej przydatne, gdyby mogły określić semantykę, która różni się od typów wbudowanych. Na przykład przydatne byłoby posiadanie typu, który wykorzystywałby arytmetykę mod-65536 niezależnie od wielkości „int”, wraz z typem, który byłby w stanie pomieścić liczby od 0 do 65535, ale mógłby dowolnie i niekoniecznie konsekwentnie być w stanie posiadania liczb większych niż to. To, jaki typ rozmiaru jest najszybszy, zależy od większości maszyn, od kontekstu, więc możliwość wyboru kompilatora byłaby optymalna dla szybkości.
supercat
204

Jak wspominasz w swoim pytaniu, nowoczesne oprogramowanie polega na współpracy między platformami i systemami w Internecie. Standardy C i C ++ podają zakresy rozmiarów liczb całkowitych, a nie konkretne rozmiary (w przeciwieństwie do języków takich jak Java i C #).

Aby mieć pewność, że twoje oprogramowanie skompilowane na różnych platformach działa z tymi samymi danymi w ten sam sposób i aby inne oprogramowanie mogło współpracować z twoim oprogramowaniem przy użyciu tych samych rozmiarów, powinieneś używać liczb całkowitych o stałym rozmiarze.

Wpisz, <cstdint>który zapewnia dokładnie to i jest standardowym nagłówkiem, który muszą zapewnić wszystkie kompilatory i standardowe platformy biblioteczne. Uwaga: ten nagłówek był wymagany tylko od C ++ 11, ale i tak zapewniało go wiele starszych implementacji bibliotek.

Chcesz 64-bitową liczbę całkowitą bez znaku? Użyj uint64_t. Podpisano 32-bitową liczbę całkowitą? Użyj int32_t. Chociaż typy w nagłówku są opcjonalne, nowoczesne platformy powinny obsługiwać wszystkie typy zdefiniowane w tym nagłówku.

Czasami potrzebna jest określona szerokość bitów, na przykład w strukturze danych wykorzystywanej do komunikacji z innymi systemami. Innym razem tak nie jest. W mniej <cstdint>wymagających sytuacjach udostępnia typy o minimalnej szerokości.

Istnieją najmniejsze warianty: int_leastXX_tbędzie liczbą całkowitą zawierającą co najmniej XX bitów. Użyje najmniejszego typu, który zapewnia XX bitów, ale typ może być większy niż określona liczba bitów. W praktyce są one zwykle takie same jak typy opisane powyżej, które podają dokładną liczbę bitów.

Istnieją również szybkie warianty: int_fastXX_tma co najmniej XX bitów, ale powinien używać typu, który działa szybko na konkretnej platformie. Definicja „szybkiego” w tym kontekście jest nieokreślona. Jednak w praktyce zazwyczaj oznacza to, że typ mniejszy niż rozmiar rejestru procesora może być aliasem do typu rozmiaru rejestru procesora. Na przykład nagłówek programu Visual C ++ 2015 określa int_fast16_t32-bitową liczbę całkowitą, ponieważ arytmetyka 32-bitowa jest ogólnie szybsza na x86 niż arytmetyka 16-bitowa.

To wszystko jest ważne, ponieważ powinieneś być w stanie używać typów, które mogą przechowywać wyniki obliczeń wykonywanych przez Twój program niezależnie od platformy. Jeśli program generuje poprawne wyniki na jednej platformie, ale niepoprawne wyniki na innej z powodu różnic w przepełnieniu liczb całkowitych, to źle. Używając standardowych typów liczb całkowitych, gwarantujesz, że wyniki na różnych platformach będą takie same w odniesieniu do wielkości użytych liczb całkowitych (oczywiście mogą występować inne różnice między platformami poza szerokością całkowitą).

Tak, longpowinien zostać zablokowany w nowoczesnym kodzie C ++. Więc należy int, shorti long long.


źródło
20
Żałuję, że nie mam jeszcze pięciu innych kont, aby móc jeszcze bardziej to zagłosować.
Steven Burnap
4
+1, miałem do czynienia z dziwnymi błędami pamięci, które występują tylko wtedy, gdy rozmiar struktury zależy od komputera, na którym kompilujesz.
Joshua Snider
9
@Wildcard to nagłówek C, który jest również częścią C ++: patrz przedrostek „c” na nim. Istnieje również sposób na umieszczenie typedefs w stdprzestrzeni nazw, gdy #included jest w jednostce kompilacji C ++, ale dokumentacja, którą podłączyłem, nie wspomina o tym, a Visual Studio wydaje się nie dbać o to, jak do nich uzyskać dostęp.
11
Zakaz intmoże być ... nadmierny? (Zastanowiłbym się, czy kod musi być wyjątkowo przenośny na wszystkich niejasnych (i nie tak niejasnych) platformach. Zakazanie go dla „kodu aplikacji” może nie sprzyjać naszym deweloperom.
Martin Ba
5
@Snowman #include <cstdint>jest zobowiązany do umieszczenia typów std::i (niestety) opcjonalnie dozwolone jest również umieszczenie ich w globalnej przestrzeni nazw. #include <stdint.h>jest dokładnie odwrotnie. To samo dotyczy każdej innej pary nagłówków C. Zobacz: stackoverflow.com/a/13643019/2757035 Chciałbym, żeby Standard wymagał, aby każdy z nich wpływał tylko na odpowiednią wymaganą przestrzeń nazw - zamiast pozornie wypaczać złe konwencje ustanowione przez niektóre implementacje - ale cóż, oto jesteśmy.
underscore_d
38

Nie, zakazanie wbudowanych typów całkowitych byłoby absurdalne. Nie należy ich jednak nadużywać.

Jeśli potrzebujesz liczby całkowitej o szerokości dokładnie N bitów, użyj (lub jeśli potrzebujesz wersji). Myślenie o 32-bitowej liczbie całkowitej i 64-bitowej liczbie całkowitej jest po prostu błędne. Może się tak zdarzyć na obecnych platformach, ale zależy to od zachowania zdefiniowanego w implementacji.std::intN_tstd::uintN_tunsignedintlong long

Używanie typów liczb całkowitych o stałej szerokości jest również przydatne do współpracy z innymi technologiami. Na przykład, jeśli niektóre części aplikacji są napisane w Javie, a inne w C ++, prawdopodobnie będziesz chciał dopasować typy liczb całkowitych, aby uzyskać spójne wyniki. (Nadal pamiętaj, że przepełnienie w Javie ma dobrze zdefiniowaną semantykę, podczas gdy signedprzepełnienie w C ++ jest niezdefiniowanym zachowaniem, więc spójność jest najwyższym celem.) Będą one również nieocenione przy wymianie danych między różnymi hostami obliczeniowymi.

Jeśli nie potrzebujesz dokładnie N bitów, a tylko wystarczająco szeroki typ , rozważ użycie (zoptymalizowanego pod kątem miejsca) lub (zoptymalizowanego pod kątem prędkości). Ponownie, obie rodziny też mają odpowiedniki.std::int_leastN_tstd::int_fastN_tunsigned

Kiedy więc używać wbudowanych typów? Ponieważ standard nie precyzuje dokładnie ich szerokości, używaj ich, gdy nie zależy ci na rzeczywistej szerokości bitów, ale na innych cechach.

A charjest najmniejszą liczbą całkowitą adresowaną przez sprzęt. Język faktycznie zmusza cię do użycia go do aliasingu dowolnej pamięci. Jest to również jedyny możliwy rodzaj reprezentacji (wąskich) ciągów znaków.

intZazwyczaj będzie najszybsza typu urządzenie może obsłużyć. Będzie wystarczająco szeroki, aby można go było ładować i przechowywać za pomocą jednej instrukcji (bez maskowania lub przesuwania bitów) i wystarczająco wąski, aby można go było obsługiwać za pomocą (najbardziej) wydajnych instrukcji sprzętowych. Dlatego intjest idealnym wyborem do przekazywania danych i wykonywania arytmetyki, gdy przepełnienie nie stanowi problemu. Na przykład domyślnym typem wyliczeń jest int. Nie zmieniaj go na 32-bitową liczbę całkowitą tylko dlatego, że możesz. Ponadto, jeśli masz wartość, która może wynosić tylko –1, 0 i 1, anintto idealny wybór, chyba że masz zamiar przechowywać ich ogromne tablice. W takim przypadku możesz użyć bardziej zwartego typu danych, kosztem zapłacenia wyższej ceny za dostęp do poszczególnych elementów. Wydajniejsze buforowanie prawdopodobnie się za to opłaci. Wiele funkcji systemu operacyjnego jest również zdefiniowanych w kategoriach int. Głupio byłoby przełożyć ich argumenty i wyniki tam iz powrotem. To wszystko, co prawdopodobnie może zrobić, to wprowadzić błędy przepełnienia.

longbędzie zwykle najszerszym typem, który można obsłużyć za pomocą instrukcji dla jednej maszyny. To sprawia, że ​​jest szczególnie unsigned longatrakcyjna do radzenia sobie z surowymi danymi i wszelkiego rodzaju manipulacjami bitowymi. Na przykład spodziewałbym się zobaczyć unsigned longimplementację wektora bitowego. Jeśli kod jest napisany ostrożnie, nie ma znaczenia, jak szeroki jest rzeczywiście typ (ponieważ kod dostosuje się automatycznie). Na platformach, na których natywnym słowem maszynowym jest 32 bity, tablica podkładowa wektora bitowego może być tablicąunsigned32-bitowe liczby całkowite są najbardziej pożądane, ponieważ głupio byłoby użyć typu 64-bitowego, który musi być ładowany za pomocą drogich instrukcji tylko w celu przesunięcia i maskowania niepotrzebnych bitów. Z drugiej strony, jeśli rodzimy rozmiar słowa platformy wynosi 64 bity, chcę tablicę tego typu, ponieważ oznacza to, że operacje takie jak „znajdź pierwszy zestaw” mogą działać nawet dwukrotnie szybciej. Zatem „problem” longopisywanego typu danych, którego rozmiar różni się w zależności od platformy, w rzeczywistości jest funkcją, którą można dobrze wykorzystać. Staje się to problemem tylko wtedy, gdy myślisz o wbudowanych typach jako typach o określonej szerokości bitów, których po prostu nie mają.

char, intI longsą bardzo użyteczne typów, jak opisano powyżej. shorti long longnie są prawie tak przydatne, ponieważ ich semantyka jest znacznie mniej wyraźna.

5gon12eder
źródło
4
OP zwrócił w szczególności uwagę na różnicę w rozmiarze longWindowsa i Uniksa. Mogę być nieporozumieniem, ale twój opis różnicy w wielkości longbycia „funkcją” zamiast „problemu” ma dla mnie sens przy porównywaniu 32- i 64-bitowych modeli danych, ale nie dla tego konkretnego porównania. Czy w tej konkretnej sprawie pytanie jest naprawdę takie? Czy może jest to cecha w innych sytuacjach (ogólnie) i nieszkodliwe w tym przypadku?
Dan Getz
3
@ 5gon12eder: Problem polega na tym, że typy takie jak uint32_t zostały utworzone w celu umożliwienia zachowania kodu niezależnego od wielkości „int”, ale brak typu, którego znaczenie byłoby „zachowuje się jak uint32_t, działa na 32- system bitowy ”sprawia, że ​​pisanie kodu, którego zachowanie jest poprawnie niezależne od rozmiaru„ int ”jest znacznie trudniejsze niż pisanie kodu, który jest prawie poprawny.
supercat
3
Tak, wiem ... stąd pochodziło przekleństwo. Oryginalni autorzy właśnie wybrali ścieżkę odporności na dzierżawę, ponieważ kiedy napisali kod, 32-bitowe systemy operacyjne były już ponad dekadę.
Steven Burnap
8
@ 5gon12eder Niestety, supercat ma rację. Wszystkie typy dokładnej szerokości są „po prostu typedefs”, a reguły promocji liczb całkowitych nie zwracają na nie uwagi, co oznacza, że ​​arytmetyka uint32_twartości będzie przeprowadzana jako podpisana , intarytmetyka szerokości na platformie, która intjest szersza niż uint32_t. (Przy dzisiejszych ABI jest to o wiele bardziej prawdopodobne, że będzie to problem uint16_t.)
zwolnić
9
Po pierwsze, dziękuję za szczegółową odpowiedź. Ale: O kochanie. Twój długi akapit: „ longzwykle będzie najszerszym typem, który można obsłużyć za pomocą instrukcji dla pojedynczej maszyny. ...” - i to jest całkowicie błędne . Spójrz na model danych Windows. IMHO, cały twój poniższy przykład się psuje, ponieważ w systemie Windows x64 długi jest nadal 32-bitowy.
Martin Ba
6

Inna odpowiedź już omawia typy cstdint i ich mniej znane odmiany.

Chciałbym dodać do tego:

użyj nazw typów specyficznych dla domeny

Oznacza to, że nie deklarują swoje parametry i zmienne być uint32_t(na pewno nie long!), Ale nazwy takie jak channel_id_type, room_count_typeetc.

o bibliotekach

Biblioteki stron trzecich, które używają longlub nie, mogą być denerwujące, szczególnie jeśli są używane jako odniesienia lub wskaźniki do nich.

Najlepszą rzeczą jest, aby owijarki.

Ogólnie rzecz biorąc, moją strategią jest stworzenie zestawu podobnych do obsady funkcji, które będą używane. Są przeciążone, aby zaakceptować tylko te typy, które dokładnie pasują do odpowiednich typów, wraz ze wszystkimi potrzebnymi wskaźnikami itp. Są zdefiniowane specyficznie dla systemu operacyjnego / kompilatora / ustawień. Pozwala to usunąć ostrzeżenia, a jednocześnie zapewnić, że używane są tylko „prawidłowe” konwersje.

channel_id_type cid_out;
...
SomeLibFoo (same_thing_really<int*>(&cid_out));

W szczególności, gdy różne typy pierwotne generują 32 bity, wybór sposobu int32_tzdefiniowania może nie pasować do wywołania biblioteki (np. Int vs long w systemie Windows).

Funkcja rzutowania dokumentuje kolizję, umożliwia sprawdzanie w czasie kompilacji wyniku pasującego do parametru funkcji i usuwa wszelkie ostrzeżenia lub błędy, jeśli tylko wtedy, gdy rzeczywisty typ odpowiada rzeczywistemu rozmiarowi. Oznacza to, że jest przeciążony i zdefiniowany, jeśli przekażę (w systemie Windows) an int*lub a long*i w przeciwnym razie poda błąd czasu kompilacji.

Tak więc, jeśli biblioteka zostanie zaktualizowana lub ktoś zmieni to channel_id_type, co jest, jest to nadal weryfikowane.

JDługosz
źródło
dlaczego głosowanie negatywne (bez komentarza)?
JDługosz
Ponieważ większość głosów negatywnych w tej sieci pojawia się bez komentarzy ...
Ruslan