Ich habe kürzlich eine Frage zum undefinierten Verhalten p < q
in C beantwortet, wenn p
und q
Zeiger auf verschiedene Objekte / Arrays sind. Das brachte mich zum Nachdenken: C ++ hat <
in diesem Fall das gleiche (undefinierte) Verhalten , bietet aber auch die Standardbibliotheksvorlage, std::less
die garantiert dasselbe zurückgibt, wie <
wenn die Zeiger verglichen werden können, und eine konsistente Reihenfolge zurückgibt, wenn sie nicht können.
Bietet C etwas mit ähnlicher Funktionalität, mit dem beliebige Zeiger (mit demselben Typ) sicher verglichen werden können? Ich habe versucht, den C11-Standard durchzusehen, und nichts gefunden, aber meine Erfahrung in C ist um Größenordnungen kleiner als in C ++, sodass ich leicht etwas übersehen hätte können.
quelle
Antworten:
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 einesuintptr_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 gleichenseg
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 darstellenSIZE_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, dassp < 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
odergs
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 unduintptr_t
in 16-Bit-Hälften aufzuteilen,seg -= off>>4; off &= 0xf;
um diese Teile dann wieder zu einer 32-Bit-Zahl zu kombinieren.quelle
p < q
ist UB in C, wenn sie auf verschiedene Objekte zeigen, nicht wahr? Ich weißp - q
ist.seg
Wert dieses Objekts und einen Versatz, der> = der Versatz innerhalb des Segments ist, in dem das Objekt beginnt. C macht es UB, viel zwischen Zeigern auf verschiedene Objekte zu tun, einschließlich Sachen wietmp = a-b
und dannb[tmp]
darauf zuzugreifena[0]
. Diese Diskussion über segmentiertes Zeiger-Aliasing ist ein gutes Beispiel dafür, warum diese Entwurfsauswahl sinnvoll ist.Ich habe einmal versucht, einen Weg zu finden, und ich habe eine Lösung gefunden, die für überlappende Objekte funktioniert und in den meisten anderen Fällen davon ausgeht, dass der Compiler das "Übliche" tut.
Sie können den Vorschlag zunächst unter Wie implementiere ich memmove in Standard C ohne Zwischenkopie? und dann, wenn das nicht funktioniert, in
uintptr
(ein Wrapper-Typ für entwederuintptr_t
oderunsigned long long
abhängig davon, obuintptr_t
verfügbar) umwandeln und ein höchstwahrscheinlich genaues Ergebnis erhalten (obwohl es wahrscheinlich sowieso keine Rolle spielt):quelle
Nein
Betrachten wir zunächst nur Objektzeiger . Funktionszeiger bringen eine ganze Reihe anderer Bedenken mit sich.
2 Zeiger
p1, p2
können unterschiedliche Codierungen haben und auf dieselbe Adresse verweisen,p1 == p2
obwohl diesmemcmp(&p1, &p2, sizeof p1)
nicht 0 ist. Solche Architekturen sind selten.Die Konvertierung dieses Zeigers in
uintptr_t
erfordert jedoch nicht dasselbe ganzzahlige Ergebnis, das zu führt(uintptr_t)p1 != (uinptr_t)p2
.(uintptr_t)p1 < (uinptr_t)p2
selbst ist wohl rechtlicher Code, der möglicherweise nicht die erhoffte Funktionalität bietet.Wenn Code wirklich nicht verwandte Zeiger vergleichen muss, bilden Sie eine Hilfsfunktion
less(const void *p1, const void *p2)
und führen Sie dort plattformspezifischen Code aus.Vielleicht:
quelle