W implementacjach z płaskim modelem pamięci (w zasadzie wszystko), rzutowanie na uintptr_t
Just Work.
(Ale zobacz, czy porównania wskaźników powinny być podpisane czy niepodpisane w 64-bitowym x86 ?, aby dowiedzieć się, czy powinieneś traktować wskaźniki jako podpisane, czy nie, w tym kwestie dotyczące tworzenia wskaźników poza obiektami, które są UB w C.)
Ale systemy z non-płaskich modeli pamięci istnieją, i myślenie o nich może pomóc wyjaśnić obecną sytuację, jak C ++ posiadające różne specyfikacje dla <
vs. std::less
.
Częścią <
wskazówek na temat oddzielania obiektów będących UB w C (lub przynajmniej nieokreślonych w niektórych wersjach C ++) jest umożliwienie dziwnych maszyn, w tym niepłaskich modeli pamięci.
Dobrze znanym przykładem jest tryb rzeczywisty x86-16, w którym wskaźniki są segmentowe: przesunięte, tworząc 20-bitowy adres liniowy za pośrednictwem (segment << 4) + offset
. Ten sam adres liniowy może być reprezentowany przez wiele różnych kombinacji seg: off.
C ++ std::less
na wskaźniki na dziwnych ISA może być kosztowne , np. „Normalizacja” segmentu: przesunięcie na x86-16, aby mieć przesunięcie <= 15. Jednak nie ma przenośnego sposobu na wdrożenie tego. Manipulacja wymagana do normalizacji uintptr_t
(lub reprezentacji obiektowej obiektu wskaźnika) jest specyficzna dla implementacji.
Ale nawet w systemach, w których C ++ std::less
musi być drogie, <
nie musi tak być. Na przykład, zakładając „duży” model pamięci, w którym obiekt mieści się w jednym segmencie, <
można po prostu porównać część przesuniętą, a nawet nie zawracać sobie głowy częścią segmentu. (Wskaźniki wewnątrz tego samego obiektu będą miały ten sam segment, w przeciwnym razie UB w C. C ++ 17 zmieniono na „nieokreślony”, co może nadal pozwalać na pominięcie normalizacji i porównywanie przesunięć.) Zakłada się, że wszystkie wskaźniki w dowolnej części obiektu zawsze używa tej samej seg
wartości, nigdy się nie normalizuje. Tego można oczekiwać od ABI w przypadku „dużej”, w przeciwieństwie do „ogromnego” modelu pamięci. (Patrz dyskusja w komentarzach ).
(Taki model pamięci może mieć na przykład maksymalny rozmiar obiektu 64 kB, ale znacznie większą maksymalną całkowitą przestrzeń adresową, która ma miejsce na wiele takich obiektów o maksymalnej wielkości. ISO C pozwala implementacjom na ograniczenie wielkości obiektu, która jest mniejsza niż maksymalna wartość (bez znaku) size_t
może reprezentować, SIZE_MAX
np. nawet w systemach z płaską pamięcią, GNU C ogranicza maksymalny rozmiar obiektu, aby PTRDIFF_MAX
obliczenia rozmiaru mogły zignorować przepełnienie podpisu.) Zobacz tę odpowiedź i dyskusję w komentarzach.
Jeśli chcesz pozwolić obiektom większym niż segment, potrzebujesz „ogromnego” modelu pamięci, który musi się martwić o przepełnienie części przesunięcia wskaźnika podczas wykonywania p++
pętli przez tablicę lub podczas wykonywania operacji arytmetycznych na indeksach / wskaźnikach. Powoduje to wszędzie wolniejszy kod, ale prawdopodobnie oznaczałoby to, p < q
że działałoby w przypadku wskaźników do różnych obiektów, ponieważ implementacja ukierunkowana na „ogromny” model pamięci normalnie wybrałaby utrzymanie normalizacji wszystkich wskaźników. Zobacz, jakie są bliskie, dalekie i ogromne wskaźniki? - niektóre prawdziwe kompilatory C dla trybu rzeczywistego x86 miały opcję kompilacji dla modelu „ogromnego”, w którym wszystkie wskaźniki domyślnie były ustawione na „ogromne”, chyba że podano inaczej.
Segmentacja w trybie rzeczywistym x86 nie jest jedynym możliwym niepłaskim modelem pamięci , jest jedynie użytecznym konkretnym przykładem ilustrującym sposób, w jaki są obsługiwane przez implementacje C / C ++. W rzeczywistości implementacje rozszerzyły ISO C o koncepcję far
vs. near
wskaźników, umożliwiając programistom wybór, kiedy mogą uciec po prostu zapisując / omijając 16-bitową część przesunięcia względem niektórych wspólnych segmentów danych.
Ale czysta implementacja ISO C musiałaby wybierać między małym modelem pamięci (wszystko oprócz kodu w tym samym 64 kB z 16-bitowymi wskaźnikami) lub dużym lub dużym, a wszystkie wskaźniki były 32-bitowe. Niektóre pętle można zoptymalizować, zwiększając tylko część odsuniętą, ale obiektów wskaźnikowych nie można zoptymalizować, aby były mniejsze.
Gdybyś wiedział, co magia manipulacja była dla danej realizacji, można wdrożyć go w czystym C . Problem polega na tym, że różne systemy używają różnych adresów, a szczegóły nie są parametryzowane przez żadne przenośne makra.
A może nie: może to obejmować wyszukiwanie czegoś ze specjalnej tablicy segmentów lub czegoś takiego, np. Tryb chroniony x86 zamiast trybu rzeczywistego, w którym częścią segmentu adresu jest indeks, a nie wartość, którą należy przesunąć w lewo. Można ustawić częściowo nakładające się segmenty w trybie chronionym, a części selektora segmentów adresów niekoniecznie będą nawet uporządkowane w tej samej kolejności, co odpowiadające im adresy podstawowe segmentów. Uzyskiwanie adresu liniowego ze wskaźnika seg: off w trybie chronionym x86 może wymagać wywołania systemowego, jeśli GDT i / lub LDT nie zostaną zmapowane na czytelne strony w twoim procesie.
(Oczywiście główne systemy operacyjne dla x86 używają płaskiego modelu pamięci, więc podstawa segmentu jest zawsze równa 0 (z wyjątkiem lokalnego przechowywania wątków przy użyciu fs
lub gs
segmentów), a tylko 32-bitowa lub 64-bitowa część „przesunięcia” jest używana jako wskaźnik .)
Możesz ręcznie dodać kod dla różnych konkretnych platform, np. Domyślnie załóż płaskie lub #ifdef
coś w celu wykrycia trybu rzeczywistego x86 i podziel uintptr_t
na 16-bitowe połówki, seg -= off>>4; off &= 0xf;
a następnie połącz te części z powrotem w 32-bitową liczbę.