Bei Implementierungen mit einem Flat-Memory-Modell (im Grunde alles) funktioniert das Casting uintptr_t
nur.
( Weitere Informationen dazu, ob Sie Zeiger als signiert behandeln sollten oder nicht, finden Sie unter Sollten Zeigervergleiche in 64-Bit-x86 signiert oder nicht signiert sein.
Aber Systeme mit nicht flachen Speichermodelle existieren, und über sie denken kann die aktuelle Situation helfen zu erklären, wie C ++ mit unterschiedlichen Spezifikationen für <
vs. std::less
.
Ein Teil des Punktes von Ein- <
Zeigern auf separate Objekte, die UB in C sind (oder zumindest in einigen C ++ - Revisionen nicht angegeben sind), besteht darin, seltsame Maschinen zu berücksichtigen, einschließlich nicht flacher Speichermodelle.
Ein bekanntes Beispiel ist der x86-16-Realmodus, bei dem Zeiger segmentweise versetzt sind und über eine lineare 20-Bit-Adresse bilden (segment << 4) + offset
. Dieselbe lineare Adresse kann durch mehrere verschiedene Seg: Off-Kombinationen dargestellt werden.
C ++ std::less
für Zeiger auf seltsamen ISAs muss möglicherweise teuer sein , z. B. ein Segment "normalisieren": Offset auf x86-16, um Offset <= 15 zu haben. Es gibt jedoch keine tragbare Möglichkeit, dies zu implementieren. Die zur Normalisierung eines uintptr_t
(oder der Objektdarstellung eines Zeigerobjekts) erforderliche Manipulation ist implementierungsspezifisch.
Aber auch auf Systemen, auf denen C ++ std::less
teuer sein muss, <
muss es nicht sein. Wenn Sie beispielsweise ein "großes" Speichermodell annehmen, bei dem ein Objekt in ein Segment passt, <
können Sie nur den versetzten Teil vergleichen und sich nicht einmal mit dem Segmentteil beschäftigen. (Zeiger innerhalb desselben Objekts haben dasselbe Segment, und ansonsten ist es UB in C. C ++ 17 wurde lediglich in "nicht angegeben" geändert, wodurch möglicherweise noch die Normalisierung übersprungen und nur Offsets verglichen werden können.) Dies setzt alle Zeiger auf einen beliebigen Teil voraus eines Objekts verwenden immer den gleichen seg
Wert, niemals normalisierend. Dies ist, was Sie von einem ABI für ein "großes" im Gegensatz zu einem "großen" Speichermodell erwarten würden. (Siehe Diskussion in Kommentaren ).
(Ein solches Speichermodell kann beispielsweise eine maximale Objektgröße von 64 KB haben, aber einen viel größeren maximalen Gesamtadressraum, der Platz für viele solcher Objekte mit maximaler Größe bietet. ISO C ermöglicht Implementierungen eine Begrenzung der Objektgröße, die niedriger als die ist Maximaler Wert (ohne Vorzeichen) size_t
kann darstellen SIZE_MAX
. Beispielsweise begrenzt GNU C selbst auf Flachspeichermodellsystemen die maximale Objektgröße auf, PTRDIFF_MAX
sodass die Größenberechnung den vorzeichenbehafteten Überlauf ignorieren kann.) Siehe diese Antwort und Diskussion in Kommentaren.
Wenn Sie Objekte zulassen möchten, die größer als ein Segment sind, benötigen Sie ein "riesiges" Speichermodell, das sich Sorgen machen muss, dass der versetzte Teil eines Zeigers beim p++
Durchlaufen eines Arrays oder bei der Indizierung / Zeigerarithmetik überläuft. Dies führt überall zu langsamerem Code, würde aber wahrscheinlich bedeuten, dass p < q
dies für Zeiger auf verschiedene Objekte funktioniert, da eine Implementierung, die auf ein "riesiges" Speichermodell abzielt, normalerweise dafür sorgt, dass alle Zeiger die ganze Zeit normalisiert bleiben. Siehe Was sind nahe, ferne und große Zeiger? - Einige echte C-Compiler für den x86-Real-Modus hatten die Option, für das "riesige" Modell zu kompilieren, bei dem alle Zeiger standardmäßig "riesig" waren, sofern nicht anders angegeben.
Die x86-Real-Mode-Segmentierung ist nicht das einzige mögliche nicht flache Speichermodell , sondern lediglich ein nützliches konkretes Beispiel, um zu veranschaulichen, wie es von C / C ++ - Implementierungen gehandhabt wird. Im wirklichen Leben erweiterten Implementierungen ISO C um das Konzept von far
vs. near
Zeigern, sodass Programmierer wählen können, wann sie davonkommen können, indem sie nur den 16-Bit-Offset-Teil relativ zu einem gängigen Datensegment speichern / weitergeben.
Eine reine ISO C-Implementierung müsste jedoch zwischen einem kleinen Speichermodell (alles außer Code in denselben 64 KB mit 16-Bit-Zeigern) oder groß oder groß wählen, wobei alle Zeiger 32-Bit sind. Einige Schleifen könnten durch Inkrementieren nur des Versatzteils optimiert werden, aber Zeigerobjekte könnten nicht so optimiert werden, dass sie kleiner sind.
Wenn Sie wüssten, was die magische Manipulation für eine bestimmte Implementierung ist, können Sie sie in reinem C implementieren . Das Problem ist, dass verschiedene Systeme unterschiedliche Adressierungen verwenden und die Details von keinem tragbaren Makro parametrisiert werden.
Oder vielleicht auch nicht: Möglicherweise müssen Sie etwas aus einer speziellen Segmenttabelle oder etwas anderem nachschlagen, z. B. den x86-geschützten Modus anstelle des Real-Modus, bei dem der Segmentteil der Adresse ein Index ist und kein Wert, der nach links verschoben werden soll. Sie könnten teilweise überlappende Segmente im geschützten Modus einrichten, und die Segmentauswahlteile von Adressen würden nicht unbedingt in derselben Reihenfolge wie die entsprechenden Segmentbasisadressen sortiert. Das Abrufen einer linearen Adresse von einem seg: off-Zeiger im x86-geschützten Modus kann einen Systemaufruf beinhalten, wenn GDT und / oder LDT in Ihrem Prozess nicht lesbaren Seiten zugeordnet sind.
(Natürlich verwenden Mainstream-Betriebssysteme für x86 ein Flat-Memory-Modell, sodass die Segmentbasis immer 0 ist (mit Ausnahme von Thread-lokalem Speicher mit fs
oder gs
Segmenten) und nur der 32-Bit- oder 64-Bit-Teil "Offset" als Zeiger verwendet wird .)
Sie können manuell Code für verschiedene spezifische Plattformen hinzufügen, z. B. standardmäßig flach annehmen oder #ifdef
etwas, um den x86-Realmodus zu erkennen und uintptr_t
in 16-Bit-Hälften aufzuteilen, seg -= off>>4; off &= 0xf;
um diese Teile dann wieder zu einer 32-Bit-Zahl zu kombinieren.