c++ - Ist das Inkrementieren eines Zeigers auf ein dynamisches Array der Größe 0 undefiniert?




pointers undefined-behavior (2)

Ich denke, Sie haben bereits die Antwort. Wenn Sie etwas genauer hinschauen: Sie haben gesagt, dass das Inkrementieren eines endlosen Iterators UB ist: Diese Antwort ist in Was ist ein Iterator?

Der Iterator ist nur ein Objekt, das einen Zeiger hat und dessen Inkrementierung den tatsächlich vorhandenen Zeiger erhöht. Daher wird ein Iterator in vielen Aspekten als Zeiger behandelt.

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // p zeigt auf das erste Element in arr

++ p; // p zeigt auf arr [1]

So wie wir Iteratoren verwenden können, um die Elemente in einem Vektor zu durchlaufen, können wir auch Zeiger verwenden, um die Elemente in einem Array zu durchlaufen. Dazu müssen wir natürlich Zeiger auf das erste und eins nach dem letzten Element erhalten. Wie wir gerade gesehen haben, können wir einen Zeiger auf das erste Element erhalten, indem wir das Array selbst verwenden oder die Adresse des ersten Elements nehmen. Wir können einen Off-the-End-Zeiger erhalten, indem wir eine andere spezielle Eigenschaft von Arrays verwenden. Wir können die Adresse des nicht existierenden Elements eins nach dem letzten Element eines Arrays nehmen:

int * e = & arr [10]; // Zeiger kurz nach dem letzten Element in arr

Hier haben wir den Indexoperator verwendet, um ein nicht vorhandenes Element zu indizieren. arr hat zehn Elemente, also befindet sich das letzte Element in arr an der Indexposition 9. Das einzige, was wir mit diesem Element tun können, ist die Adresse zu ermitteln, die wir tun, um e zu initialisieren. Wie ein Off-the-End-Iterator (§ 3.4.1, S. 106) zeigt ein Off-the-End-Zeiger nicht auf ein Element. Infolgedessen dürfen wir einen Off-the-End-Zeiger nicht dereferenzieren oder inkrementieren.

Dies ist aus der C ++ Primer 5 Edition von Lipmann.

Also ist es UB, tu es nicht.

AFAIK, wir können zwar kein statisches Speicherarray der Größe 0 erstellen, aber wir können es mit dynamischen Arrays tun:

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

Wie ich gelesen habe, verhält sich p wie ein One-Past-End-Element. Ich kann die Adresse drucken, auf die p zeigt.

if(p)
    cout << p << endl;
  • Ich bin mir zwar sicher, dass wir diesen Zeiger (last-last-element) nicht dereferenzieren können, wie wir es mit Iteratoren (last-last-element) nicht können, aber was ich nicht sicher bin, ist, ob dieser Zeiger erhöht wird p ? Ist ein undefiniertes Verhalten (UB) wie bei Iteratoren?

    p++; // UB?

Im engeren Sinne handelt es sich nicht um Undefiniertes Verhalten, sondern um implementierungsdefiniertes Verhalten. Obwohl es nicht ratsam ist, Nicht-Mainstream-Architekturen zu unterstützen, können Sie dies wahrscheinlich tun.

Das Standard-Zitat von interjay ist gut und zeigt UB an, aber es ist meiner Meinung nach nur der zweitbeste Treffer, da es sich um Zeiger-Zeiger-Arithmetik handelt (komischerweise ist eines explizit UB, das andere nicht). Es gibt einen Absatz, der sich direkt mit der Operation befasst:

[expr.post.incr] / [expr.pre.incr]
Der Operand soll [...] oder ein Zeiger auf einen vollständig definierten Objekttyp sein.

Oh, Moment mal, ein vollständig definierter Objekttyp? Das ist alles? Ich meine wirklich, Typ ? Sie brauchen also überhaupt kein Objekt?
Es ist ziemlich viel Zeit zum Lesen erforderlich, um tatsächlich einen Hinweis darauf zu finden, dass etwas darin möglicherweise nicht so genau definiert ist. Denn bisher liest es sich so, als dürfe man es einwandfrei machen, ohne Einschränkungen.

[basic.compound] 3 gibt eine Aussage darüber, welchen [basic.compound] 3 man haben kann, und da keiner der anderen drei das Ergebnis Ihrer Operation ist, würde diese eindeutig unter 3.4 fallen: ungültiger Zeiger .
Es heißt jedoch nicht, dass Sie keinen ungültigen Zeiger haben dürfen. Im Gegenteil, es werden einige sehr häufige, normale Bedingungen aufgelistet (z. B. das Ende der Speicherdauer), bei denen Zeiger regelmäßig ungültig werden. Das ist also anscheinend erlaubt. Und in der Tat:

[basic.stc] 4
Die Indirektion durch einen ungültigen Zeigerwert und die Übergabe eines ungültigen Zeigerwerts an eine Aufhebungsfunktion haben ein undefiniertes Verhalten. Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten.

Wir machen dort ein "beliebiges anderes", es ist also kein undefiniertes Verhalten, sondern implementierungsdefiniert, also generell zulässig (es sei denn, die Implementierung sagt ausdrücklich etwas anderes aus).

Leider ist das nicht das Ende der Geschichte. Obwohl sich das Nettoergebnis von nun an nicht mehr ändert, wird es umso verwirrender, je länger Sie nach "pointer" suchen:

[basic.compound]
Ein gültiger Wert eines Objektzeigertyps repräsentiert entweder die Adresse eines Bytes im Speicher oder einen Nullzeiger. Befindet sich ein Objekt vom Typ T an einer Adresse, [...] soll A auf dieses Objekt verweisen, unabhängig davon, wie der Wert erhalten wurde .
[Hinweis: Beispielsweise wird angenommen, dass die Adresse 1 nach dem Ende eines Arrays auf ein nicht verwandtes Objekt des Elementtyps des Arrays verweist, das sich möglicherweise an dieser Adresse befindet. [...]].

Lies als: OK, wen interessiert das? Solange ein Zeiger irgendwo in Erinnerung bleibt , bin ich gut?

[basic.stc.dynamic.safety] Ein Zeigerwert ist ein sicher abgeleiteter Zeiger [bla bla]

Lesen Sie als: OK, sicher abgeleitet, was auch immer. Es erklärt weder, was das ist, noch sagt es, dass ich es wirklich brauche. Sicher abgeleitet zum Teufel. Anscheinend kann ich immer noch nicht sicher abgeleitete Zeiger haben, ganz gut. Ich vermute, dass eine Dereferenzierung wahrscheinlich keine so gute Idee wäre, aber es ist durchaus zulässig, sie zu haben. Es heißt nicht anders.

Eine Implementierung kann eine verringerte Zeigersicherheit aufweisen. In diesem Fall hängt die Gültigkeit eines Zeigerwerts nicht davon ab, ob es sich um einen sicher abgeleiteten Zeigerwert handelt.

Oh, also ist es vielleicht egal, was ich dachte. Aber warte ... "darf nicht"? Das heißt, es kann auch . Wie soll ich wissen?

Alternativ kann eine Implementierung eine strikte Zeigersicherheit aufweisen. In diesem Fall ist ein Zeigerwert, der kein sicher abgeleiteter Zeigerwert ist, ein ungültiger Zeigerwert, es sei denn, das referenzierte vollständige Objekt hat eine dynamische Speicherdauer und wurde zuvor für erreichbar erklärt

Warten Sie, es ist sogar möglich, dass ich declare_reachable() für jeden Zeiger aufrufen declare_reachable() . Wie soll ich wissen?

Jetzt können Sie in intptr_t konvertieren, was genau definiert ist und eine ganzzahlige Darstellung eines sicher abgeleiteten Zeigers ergibt. Da es sich natürlich um eine Ganzzahl handelt, ist es absolut legitim und genau definiert, sie nach Belieben zu erhöhen.
Und ja, Sie können intptr_t zurück in einen Zeiger konvertieren, der ebenfalls gut definiert ist. Nur gerade, da dies nicht der ursprüngliche Wert ist, kann nicht mehr garantiert werden, dass Sie (offensichtlich) einen sicher abgeleiteten Zeiger haben. Alles in allem ist dies, obwohl implementiert, zu 100% legitim:

[expr.reinterpret.cast] 5
Ein Wert vom Typ Integral oder vom Typ Aufzählung kann explizit in einen Zeiger konvertiert werden. Ein Zeiger, der in eine Ganzzahl mit ausreichender Größe [...] und zurück auf denselben Zeigertyp-Originalwert konvertiert wurde; Zuordnungen zwischen Zeigern und Ganzzahlen sind ansonsten durch die Implementierung definiert.

Der Fang

Zeiger sind nur gewöhnliche Ganzzahlen, nur Sie verwenden sie zufällig als Zeiger. Oh, wenn das nur wahr wäre!
Leider gibt es Architekturen, in denen dies überhaupt nicht zutrifft, und das bloße Erzeugen eines ungültigen Zeigers (nicht dereferenzieren, nur in einem Zeigerregister) wird eine Falle verursachen.

Das ist also die Basis der "definierten Implementierung". Dies und die Tatsache, dass das Inkrementieren eines Zeigers, wann immer Sie möchten, natürlich zu einem Überlauf führen kann, mit dem der Standard nicht umgehen möchte. Das Ende des Adressraums der Anwendung stimmt möglicherweise nicht mit dem Ort des Überlaufs überein, und Sie wissen nicht einmal, ob für Zeiger auf eine bestimmte Architektur ein Überlauf vorliegt. Alles in allem ist es ein albtraumhaftes Durcheinander, in keiner Beziehung zu den möglichen Vorteilen.

Der Umgang mit der One-Past-Object-Bedingung ist hingegen einfach: Die Implementierung muss lediglich sicherstellen, dass niemals ein Objekt zugewiesen wird, damit das letzte Byte im Adressraum belegt ist. Das ist klar definiert, da es nützlich und trivial ist, dies zu garantieren.





dynamic-arrays