Czy typy o zmiennej szerokości zostały zastąpione przez typy stałe we współczesnym C?

21

Interesujący punkt natknąłem się dzisiaj na recenzję Code Review . @ Veedrac zalecił w tej odpowiedzi, aby zmienne typy rozmiarów (np. intI long) zostały zastąpione stałymi typami rozmiarów, takimi jak uint64_ti uint32_t. Cytat z komentarzy do tej odpowiedzi:

Rozmiary int i long (a zatem wartości, które mogą przechowywać) są zależne od platformy. Z drugiej strony int32_t ma zawsze długość 32 bitów. Użycie int oznacza po prostu, że twój kod działa inaczej na różnych platformach, co na ogół nie jest tym, czego chcesz.

Powód, dla którego norma nie określa typowych typów, jest częściowo wyjaśniony tutaj przez @supercat. C został napisany jako przenośny w różnych architekturach, w przeciwieństwie do asemblera, który zwykle był używany do programowania systemów w tym czasie.

Myślę, że pierwotnie zamierzeniem projektu było, aby każdy typ inny niż int był najmniejszą rzeczą, która mogła poradzić sobie z liczbami o różnych rozmiarach, i że był to najbardziej praktyczny rozmiar „ogólnego zastosowania”, który mógłby obsłużyć +/- 32767.

Co do mnie, zawsze korzystałem inti nie martwiłem się o alternatywy. Zawsze uważałem, że jest to najbardziej typ z najlepszą wydajnością, koniec historii. Jedynym miejscem, które moim zdaniem przydałaby się stała szerokość, byłoby przydatne do kodowania danych w celu przechowywania lub przesyłania przez sieć. Rzadko też widziałem typy o stałej szerokości w kodzie napisanym przez innych.

Czy utknąłem w latach 70., czy jest uzasadnienie dla zastosowania intw erze C99 i później?

jacwah
źródło
1
Część ludzi po prostu naśladuje innych. Uważam, że większość kodu typu bit-ustalony została pozbawiona świadomości. Nie ma powodu, aby ustawiać rozmiar ani nie. Mam kod wykonany głównie na platformach 16-bitowych (MS-DOS i Xenix z lat 80.), które po prostu kompilują się i działają dzisiaj na dowolnych 64 bitach, a korzyści płynące z nowego rozmiaru słowa i adresowania, po prostu go kompilują. To znaczy, że serializacja w celu eksportowania / importowania danych jest bardzo ważnym projektem architektury, który zapewnia ich przenośność.
Luciano

Odpowiedzi:

7

Istnieje powszechny i ​​niebezpieczny mit, który pozwala uint32_tuchronić programistów przed martwieniem się wielkością int. Chociaż byłoby pomocne, gdyby Komitet Normalizacyjny określił sposób deklarowania liczb całkowitych za pomocą semantyki niezależnej od maszyny, typy niepodpisane, takie jak uint32_tsemantyka, są zbyt luźne, aby umożliwić pisanie kodu w sposób, który jest zarówno czysty, jak i przenośny; ponadto, podpisane typy, takie jak int32semantyka, dla wielu aplikacji są niepotrzebnie ściśle zdefiniowane, a tym samym wykluczają przydatne optymalizacje.

Rozważ na przykład:

uint32_t upow(uint32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

int32_t spow(int32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

Na maszynach gdzie int albo nie można przechowywać 4294967295, albo można przechowywać 18446744065119617025, pierwsza funkcja zostanie zdefiniowana dla wszystkich wartości ni exponent, a na jej zachowanie nie będzie miała wpływu wielkość int; Ponadto, średnia nie wymaga się, że dają to różne zachowanie na maszynach o dowolnym rozmiarze int Niektóre wartości ni exponent, jednakże powoduje to powołać niezdefiniowane Zachowanie w przypadku maszyn 4294967295 odwzorowane jako intale +18446744065119617025 nie.

Druga funkcja przyniesie nieokreślone zachowanie dla niektórych wartości n i exponentna komputerach, na których intnie może przechowywać 4611686014132420609, ale zapewni określone zachowanie dla wszystkich wartości na ni exponentna wszystkich komputerach, na których jest to możliwe (specyfikacje int32_tsugerują, że zachowanie owijania uzupełnień dwóch na komputerach, na których jest to możliwe jest mniejszy niż int).

Historycznie, mimo że Standard nie mówił nic o tym, co kompilatory powinny zrobić int przepełnieniem upow, kompilatory konsekwentnie dawałyby takie samo zachowanie, jakby intbyły wystarczająco duże, aby się nie przepełnić. Niestety, niektóre nowsze kompilatory mogą próbować „optymalizować” programy poprzez wyeliminowanie zachowań, które nie są wymagane przez standard.

supercat
źródło
3
Każdy, kto chce ręcznie wdrożyć pow, pamiętaj, że ten kod jest tylko przykładem i nie obsługuje exponent=0!
Mark Hurd
1
Myślę, że powinieneś używać operatora dekrementacji prefiksu, a nie postfiksa, obecnie robi to 1 dodatkowe mnożenie, np. exponent=1Spowoduje to, że n zostanie pomnożone samo, ponieważ dekrementacja jest wykonywana po sprawdzeniu, jeśli przyrost jest wykonywany przed sprawdzeniem ( tj. --exponent), żadne mnożenie nie zostanie wykonane, a samo n zostanie zwrócone.
ALXGTV
2
@MarkHurd: Funkcja ma słabą nazwę, ponieważ tak naprawdę jest N^(2^exponent)obliczana, ale obliczenia postaci N^(2^exponent)są często używane do obliczania funkcji potęgowania, a potęgowanie mod-4294967296 jest przydatne do takich rzeczy, jak obliczanie skrótu konkatenacji dwóch ciągów znaków, których skróty są znane.
supercat
1
@ALXGTV: Ta funkcja miała ilustrować coś, co obliczyło coś związanego z mocą. To, co faktycznie oblicza, to wykładnik N ^ (2 ^), który jest częścią wydajnie obliczającego wykładnik N ^, i może się nie powieść, nawet jeśli N jest mały (powtarzane mnożenie uint32_tprzez 31 nigdy nie da UB, ale sprawny sposób na obliczenie 31 ^ N pociąga za sobą obliczenia 31 ^ (2 ^ N), które będą
supercat
Nie sądzę, żeby to był dobry argument. Celem nie jest zdefiniowanie funkcji dla wszystkich danych wejściowych, sensownych czy nie; ma być w stanie uzasadnić rozmiary i przepełnienie. int32_tczasami zdefiniowanie przepełnienia, a czasem nie, o czym zdajesz się wspominać, wydaje się mieć minimalne znaczenie w związku z faktem, że pozwala mi to przede wszystkim na zapobieganie przepełnieniu. A jeśli chcesz zdefiniowane przepełnienie, istnieje prawdopodobieństwo, że chcesz modulo wyniku jakiejś stałej wartości - więc i tak używasz typów o stałej szerokości.
Veedrac,
4

W przypadku wartości ściśle związanych ze wskaźnikami (a tym samym z ilością pamięci adresowalnej), takich jak rozmiary buforów, indeksy tablic i Windows ' lParam, sensowne jest posiadanie typu liczby całkowitej o rozmiarze zależnym od architektury. Zatem typy o zmiennej wielkości są nadal przydatne. To dlatego mamy typedefs size_t, ptrdiff_t,intptr_t , itd. Oni mają być typedefs ponieważ żaden z wbudowanego C całkowitą typów musi być wskaźnik wielkości.

Więc pytanie brzmi, czy naprawdę char, short,int , long, i long longsą nadal użyteczne.

IME, nadal jest powszechne w programach C i C ++ do intwiększości zastosowań. I przez większość czasu (tj. Gdy twoje liczby mieszczą się w zakresie ± 32 767 i nie masz rygorystycznych wymagań dotyczących wydajności), działa to dobrze.

Ale co, jeśli chcesz pracować z liczbami w przedziale 17–32 bitów (np. Populacje dużych miast)? Możesz użyć int, ale byłoby to twarde zakodowanie zależności platformy. Jeśli chcesz ściśle przestrzegać standardu, możesz użyćlong , co gwarantuje co najmniej 32 bity.

Problem polega na tym, że standard C nie określa żadnego maksymalnego rozmiaru dla typu liczb całkowitych. Istnieją implementacje, w których longjest 64 bitów, co podwaja zużycie pamięci. A jeśli telong będą to elementy tablicy z milionami przedmiotów, będziesz szaleć pamięć.

Tak więc, ani intnie longjest odpowiednim typem do użycia tutaj, jeśli chcesz, aby Twój program był zarówno wieloplatformowy, jak i efektywny pod względem pamięci. Enter int_least32_t.

  • Twój kompilator I16L32 oferuje wersję 32-bitową long , unikając problemów obcięcieint
  • Twój kompilator I32L64 zapewnia wersję 32-bitową int, co pozwala uniknąć marnowania pamięci w wersji 64-bitowejlong .
  • Twój kompilator I36L72 oferuje 36-bit int

OTOH, załóżmy, że nie potrzebujesz wielkich liczb lub wielkich tablic, ale potrzebujesz prędkości. I intmoże być wystarczająco duży na wszystkich platformach, ale niekoniecznie jest to najszybszy typ: systemy 64-bitowe zwykle nadal mają wersję 32-bitową int. Ale można użyć int_fast16_ti uzyskać „najszybszy” typu, czy to int, longczy long long.

Istnieją więc praktyczne zastosowania dla typów z <stdint.h>. Standardowe typy liczb całkowitych nic nie znaczą . Zwłaszcza long, który może mieć 32 lub 64 bity i może, ale nie musi być wystarczająco duży, aby pomieścić wskaźnik, w zależności od kaprysu twórców kompilatora.

dan04
źródło
Problem z takimi typami uint_least32_tjest taki, że ich interakcje z innymi typami są jeszcze słabiej określone niż w przypadku uint32_t. IMHO, Standard powinien definiować typy takie jak uwrap32_tiz unum32_tsemantyką, którą każdy kompilator, który definiuje typ uwrap32_t, musi promować jako typ bez znaku w zasadniczo tych samych przypadkach, w których byłby promowany, gdyby intmiały 32 bity, a każdy kompilator, który definiuje typ, unum32_tmusi zapewnić, że podstawowe promocje arytmetyczne zawsze przekształcają go w typ podpisany, który jest w stanie utrzymać swoją wartość.
supercat,
Ponadto standard może również definiować typy, których przechowywanie i aliasing były zgodne z intN_ti uintN_t, i których zdefiniowane zachowania byłyby spójne z intN_ti uintN_t, ale które dawałyby kompilatorom pewną swobodę w przypadku, gdyby kodowi przypisano wartości spoza ich zakresu [dopuszczając semantykę podobną do tych, które były być może przeznaczony do uint_least32_t, ale bez niejasności, takich jak to, czy dodanie a uint_least16_ti an int32_tprzyniosłoby podpisany lub usnted wynik.
supercat,