size_t vs. uintptr_t

246

Standard C gwarantuje, że size_tjest to typ, który może przechowywać dowolny indeks tablicy. Oznacza to, że logicznie size_tpowinno być w stanie pomieścić dowolny typ wskaźnika. Czytałem na niektórych stronach, które znalazłem w Googles, że jest to legalne i / lub powinno zawsze działać:

void *v = malloc(10);
size_t s = (size_t) v;

Zatem w C99 standard wprowadził typy intptr_ti uintptr_t, które są podpisane, a typy niepodpisane gwarantują, że będą mogły przechowywać wskaźniki:

uintptr_t p = (size_t) v;

Jaka jest różnica między używaniem size_ta uintptr_t? Oba są niepodpisane i oba powinny być w stanie pomieścić dowolny typ wskaźnika, więc wydają się funkcjonalnie identyczne. Czy istnieje jakiś naprawdę ważny powód, aby używać uintptr_t(lub jeszcze lepiej: a void *) zamiast a size_t, innego niż jasność? Czy w nieprzejrzystej strukturze, w której pole będzie obsługiwane tylko przez funkcje wewnętrzne, czy jest jakiś powód, aby tego nie robić?

Z tego samego powodu, ptrdiff_tczy był typem podpisanym zdolnym do utrzymywania różnic kursorów, a zatem zdolnym do utrzymywania większości wskaźników, więc czym się różni intptr_t?

Czy wszystkie te typy nie służą w zasadzie trywialnie różnym wersjom tej samej funkcji? Jeśli nie to dlaczego? Co nie mogę zrobić z jednym z nich, czego nie mogę zrobić z innym? Jeśli tak, to dlaczego C99 dodał dwa zasadniczo zbędne typy do języka?

Jestem gotów zignorować wskaźniki funkcji, ponieważ nie dotyczą one obecnego problemu, ale śmiało mogę o nich wspomnieć, ponieważ mam podejrzenia, że ​​będą kluczowe dla „poprawnej” odpowiedzi.

Chris Lutz
źródło

Odpowiedzi:

236

size_tjest typem, który może przechowywać dowolny indeks tablicy. Oznacza to, że logicznie rozmiar_t powinien być w stanie pomieścić dowolny typ wskaźnika

Niekoniecznie! Wróćmy na przykład do czasów segmentowanych architektur 16-bitowych: tablica może być ograniczona do jednego segmentu (tak size_tby zrobił 16-bitowy ) ALE możesz mieć wiele segmentów (więc intptr_tdo wybrania musiałby być typ 32-bitowy segment oraz przesunięcie w nim). Wiem, że te rzeczy brzmią dziwnie w czasach jednolicie adresowalnych niesegmentowanych architektur, ale standardowy MUSI zaspokoić szerszą gamę niż „co jest normalne w 2009 roku”, wiesz!)

Alex Martelli
źródło
6
To, wraz z wieloma innymi, którzy wzrosła do tego samego wniosku, wyjaśnia różnicę między size_ta uintptr_tale co ptrdiff_ti intptr_t- nie oba z nich będzie w stanie przechowywać ten sam zakres wartości niemal na każdej platformie? Dlaczego mamy zarówno podpisane, jak i niepodpisane typy liczb całkowitych wielkości wskaźnika, szczególnie jeśli ptrdiff_tjuż służą celowi podpisanego typu liczby całkowitej.
Chris Lutz
8
Kluczowe zdanie znajduje się „na prawie każdej platformie”, @Chris. Implementacja może ograniczyć wskaźniki do zakresu 0xf000-0xffff - wymaga to 16-bitowego intptr_t, ale tylko 12/13-bitowego ptrdiff_t.
paxdiablo
29
@Chris, tylko dla wskaźników w tej samej tablicy jest dobrze zdefiniowane, aby wziąć ich różnicę. Tak więc, na dokładnie tych samych segmentowanych 16-bitowych architekturach (tablica musi znajdować się w jednym segmencie, ale dwie różne tablice mogą znajdować się w różnych segmentach) wskaźniki muszą mieć 4 bajty, ale różnice wskaźnika mogą wynosić 2 bajty!
Alex Martelli
6
@AlexMartelli: Z wyjątkiem tego, że różnice wskaźnika mogą być dodatnie lub ujemne. Standard wymaga size_tco najmniej 16 bitów, ale ptrdiff_tco najmniej 17 bitów (co w praktyce oznacza, że ​​prawdopodobnie będzie to co najmniej 32 bity).
Keith Thompson
3
Bez względu na segmentowane architektury, a co z nowoczesną architekturą, taką jak x86-64? Wczesne implementacje tej architektury dają tylko 48-bitową przestrzeń adresowalną, ale same wskaźniki są 64-bitowym typem danych. Największy ciągły blok pamięci, który można rozsądnie adresować, to 48-bit, więc wyobrażam sobie, SIZE_MAXże nie powinien wynosić 2 ** 64. Pamiętaj, że jest to adresowanie płaskie. segmentacja nie jest konieczna, aby uzyskać niedopasowanie między SIZE_MAXwskaźnikiem danych a zakresem.
Andon M. Coleman
89

Jeśli chodzi o twoje oświadczenie:

„Standard C gwarantuje, że size_tjest to typ, który może przechowywać dowolny indeks tablicy. Oznacza to, że logicznie size_tpowinien być w stanie przechowywać dowolny typ wskaźnika”.

Jest to w rzeczywistości błąd (nieporozumienie wynikające z niewłaściwego rozumowania) (a) . Możesz myśleć, że to drugie wynika z pierwszego, ale tak nie jest.

Wskaźniki i indeksy tablicowe to nie to samo. Można sobie wyobrazić zgodną implementację, która ogranicza tablice do 65536 elementów, ale pozwala wskaźnikom na adresowanie dowolnej wartości w ogromnej 128-bitowej przestrzeni adresowej.

C99 stwierdza, że ​​górna granica size_tzmiennej jest zdefiniowana przez SIZE_MAXi może wynosić nawet 65535 (patrz C99 TR3, 7.18.3, niezmieniony w C11). Wskaźniki byłyby dość ograniczone, gdyby były ograniczone do tego zakresu w nowoczesnych systemach.

W praktyce prawdopodobnie okaże się, że twoje założenie się utrzymuje, ale nie dlatego, że standard to gwarantuje. Ponieważ tak naprawdę to nie gwarantuje.


(a) Nawiasem mówiąc, nie jest to jakaś forma osobistego ataku, tylko stwierdzenie, dlaczego twoje wypowiedzi są błędne w kontekście krytycznego myślenia. Na przykład następujące rozumowanie jest również nieprawidłowe:

Wszystkie szczenięta są słodkie. To jest słodkie. Dlatego to musi być szczeniak.

Bystry lub inny charakter szczeniąt nie ma tutaj znaczenia, wszystko, co stwierdzam, to fakt, że dwa fakty nie prowadzą do wniosku, ponieważ dwa pierwsze zdania pozwalają na istnienie uroczych rzeczy, które nie są szczeniętami.

Jest to podobne do pierwszego stwierdzenia, które niekoniecznie nakazuje drugie.

paxdiablo
źródło
Zamiast przepisać to, co powiedziałem w komentarzach do Alexa Martellego, powiem tylko dzięki za wyjaśnienie, ale powtórzę drugą część mojego pytania ( część ptrdiff_tvs. intptr_t).
Chris Lutz
5
@ Ivan, podobnie jak większość komunikacji, konieczne jest wspólne zrozumienie niektórych podstawowych elementów. Jeśli uznasz tę odpowiedź za „wyśmiewanie się”, zapewniam cię, że to nieporozumienie z moją intencją. Zakładając, że odwołujesz się do mojego komentarza „logicznego błędu” (nie widzę żadnej innej możliwości), miało to być stwierdzenie oparte na faktach, a nie na oświadczeniu złożonym na koszt PO. Jeśli chcesz zasugerować jakąś konkretną poprawę w celu zminimalizowania możliwości nieporozumień (zamiast zwykłej skargi), chętnie się zastanowię.
paxdiablo
1
@ivan_pozdeev - to obrzydliwa i drastyczna para zmian, i nie widzę dowodów na to, że paxdiablo „naśmiewał się” z każdego. Gdybym był OP, wycofałbym to od razu ...
ex nihilo,
1
@Ivan, niezadowolony z proponowanych przez Ciebie zmian, wycofał się, a także próbował usunąć wszelkie niezamierzone przestępstwa. Jeśli masz jakieś inne zmiany do zaoferowania, sugeruję rozpoczęcie czatu, abyśmy mogli porozmawiać.
paxdiablo
1
@paxdiablo w porządku, myślę, że „to rzeczywiście błąd” jest mniej protekcjonalny.
ivan_pozdeev
36

Pozwolę, aby wszystkie pozostałe odpowiedzi były uzasadnione ograniczeniami segmentów, egzotycznymi architekturami i tak dalej.

Czy prosta różnica w nazwach nie jest wystarczającym powodem, aby użyć właściwego typu dla właściwej rzeczy?

Jeśli przechowujesz rozmiar, użyj size_t. Jeśli przechowujesz wskaźnik, użyj intptr_t. Osoba czytająca Twój kod od razu wie, że „aha, to jest rozmiar czegoś, prawdopodobnie w bajtach”, i „och, z jakiegoś powodu tutaj jest przechowywana wartość wskaźnika jako liczba całkowita”.

W przeciwnym razie możesz po prostu użyć unsigned long(lub, w dzisiejszych czasach, unsigned long long) wszystkiego. Rozmiar to nie wszystko, nazwy typów mają znaczenie, które jest przydatne, ponieważ pomaga opisać program.

rozwijać
źródło
Zgadzam się, ale zastanawiałem się nad czymś w rodzaju hacka / sztuczki (którą oczywiście oczywiście udokumentuję) polegającą na przechowywaniu typu wskaźnika w size_tpolu.
Chris Lutz
@MarkAdler Standard nie wymaga, aby wskaźniki były reprezentowalne jako liczby całkowite: dowolny typ wskaźnika można przekonwertować na typ całkowity. Z wyjątkiem przypadków określonych wcześniej, wynik jest zdefiniowany w implementacji. Jeśli wyniku nie można przedstawić w postaci liczby całkowitej, zachowanie jest niezdefiniowane. Wynik nie musi znajdować się w zakresie wartości dowolnego typu liczby całkowitej. Tak więc, tylko void*, intptr_ti uintptr_tsą gwarantowane, aby móc reprezentować dowolny wskaźnik do danych.
Andrew Svietlichnyy
12

Możliwe, że rozmiar największej tablicy jest mniejszy niż wskaźnik. Pomyśl o architekturach podzielonych na segmenty - wskaźniki mogą być 32-bitowe, ale pojedynczy segment może być w stanie adresować tylko 64 KB (na przykład stara architektura 8086 w trybie rzeczywistym).

Chociaż nie są one już powszechnie używane w komputerach stacjonarnych, standard C ma obsługiwać nawet małe, wyspecjalizowane architektury. Nadal rozwijane są systemy na przykład z 8 lub 16 bitowymi procesorami.

Michael Burr
źródło
Ale możesz indeksować wskaźniki tak jak tablice, więc size_tteż powinieneś sobie z tym poradzić? A może tablice dynamiczne w jakimś odległym segmencie nadal byłyby ograniczone do indeksowania w obrębie tego segmentu?
Chris Lutz
Indeksowanie wskaźników jest technicznie obsługiwane tylko do rozmiaru tablicy, na którą wskazują - więc jeśli tablica jest ograniczona do rozmiaru 64 KB, to wszystko, co arytmetyka wskaźników musi obsługiwać. Jednak kompilatory MS-DOS wspierały „ogromny” model pamięci, w którym manipulowano dalekimi wskaźnikami (wskaźniki segmentowe 32-bitowe), aby mogły zająć się całą pamięcią jako pojedynczą tablicą - ale arytmetyka zastosowana do wskaźników za scenami była całkiem brzydkie - kiedy przesunięcie zwiększało się powyżej wartości 16 (lub czegoś), przesunięcie było zawijane z powrotem do 0, a część segmentu była zwiększana.
Michael Burr,
7
Przeczytaj en.wikipedia.org/wiki/C_memory_model#Memory_segmentation i płacz dla programistów MS-DOS, którzy zginęli, abyśmy mogli być wolni.
Justicle,
Gorzej, że funkcja stdlib nie zajęła się słowem kluczowym OGROMNYM. MS-C 16-bitowej dla strfunkcji Borland nawet dla memfunkcji ( memset, memcpy, memmove). Oznaczało to, że mogłeś zastąpić część pamięci, gdy przesunęło się przesunięcie, fajnie było debugować na naszej wbudowanej platformie.
Patrick Schlüter,
@Justicle: Architektura segmentowa 8086 nie jest dobrze obsługiwana w C, ale nie znam żadnej innej architektury, która byłaby bardziej wydajna w przypadkach, w których przestrzeń adresowa 1 MB jest wystarczająca, ale 64K nie byłaby. Niektóre współczesne maszyny JVM faktycznie używają adresowania bardzo podobnie do trybu rzeczywistego x86, używając przesunięcia 32-bitowych odniesień do obiektów pozostawiając 3 bity, aby wygenerować adresy bazowe obiektów w przestrzeni adresowej 32 GB.
supercat
5

Wyobrażam sobie (i dotyczy to wszystkich nazw typów), że lepiej oddaje twoje intencje w kodzie.

Na przykład, mimo że unsigned shorti wchar_tsą tego samego rozmiaru w systemie Windows (myślę), użycie wchar_tzamiast unsigned shortpokazuje zamiar użycia go do przechowywania szerokiego znaku, a nie tylko dowolnej liczby.

dreamlax
źródło
Ale jest tutaj różnica - w moim systemie wchar_tjest znacznie większa niż unsigned shorttak, więc użycie jednego do drugiego byłoby błędne i spowodowałoby poważny (i nowoczesny) problem z przenośnością, podczas gdy problemy z przenośnością między size_ti uintptr_twydają się leżeć w odległych krajach z 1980-coś (losowe dźgnięcie w ciemność na randkę, tam)
Chris Lutz
Touché! Ale z drugiej strony size_ti uintptr_tnadal sugerują użycie w swoich nazwach.
dreamlax
Robią to, a ja chciałem wiedzieć, czy jest to uzasadnione poza zwykłą jasnością. I okazuje się, że jest.
Chris Lutz
3

Patrząc zarówno do tyłu, jak i do przodu, i przypominając sobie, że różne architektury dziwaków były rozproszone po krajobrazie, jestem prawie pewien, że próbowali zawinąć wszystkie istniejące systemy, a także zapewnić wszystkie możliwe przyszłe systemy.

Tak więc, w sposób ustalony, do tej pory nie potrzebowaliśmy tak wielu typów.

Ale nawet w LP64, dość powszechnym paradygmacie, potrzebowaliśmy size_t i ssize_t dla interfejsu wywołania systemowego. Można sobie wyobrazić bardziej ograniczony system przyszłości lub przyszłości, w którym użycie pełnego 64-bitowego typu jest kosztowne i mogą chcieć korzystać z operacji we / wy większych niż 4 GB, ale nadal mają wskaźniki 64-bitowe.

Myślę, że musisz się zastanawiać: co mogło zostać opracowane, co może przyjść w przyszłości. (Być może 128-bitowe wskaźniki dla całego systemu rozproszonego w Internecie, ale nie więcej niż 64 bity w wywołaniu systemowym, a może nawet „32-bitowy” starszy limit :-) Obraz, że starsze systemy mogą uzyskać nowe kompilatory C. .

Zobacz także, co wtedy istniało. Oprócz modeli pamięci w prawdziwym trybie zillion 286, co powiesz na 60-bitowe mainframe słowa / 18-bitowego CDC? A co z serią Cray? Nie wspominając o normalnym ILP64, LP64, LLP64. (Zawsze myślałem, że Microsoft był pretensjonalny z LLP64, powinien to być P64). Z pewnością mogę sobie wyobrazić komitet próbujący objąć wszystkie bazy ...

DigitalRoss
źródło
-9
int main(){
  int a[4]={0,1,5,3};
  int a0 = a[0];
  int a1 = *(a+1);
  int a2 = *(2+a);
  int a3 = 3[a];
  return a2;
}

Oznacza to, że intptr_t musi zawsze zastępować size_t i visa versa.

Chris Becke
źródło
10
Wszystkie te pokazy są szczególnym dziwactwem składniowym C. Indeksowanie tablicy jest zdefiniowane w taki sposób, że x [y] jest równoważne * (x + y), a ponieważ + 3 i 3 + a są identyczne pod względem typu i wartości, możesz użyj 3 [a] lub [3].
Fred Nurk