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

34

AFAIK, obwohl wir kein statisches Speicherarray der Größe 0 erstellen können, können wir dies mit dynamischen tun:

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

Wie ich gelesen habe, pverhält es sich wie ein One-Past-End-Element. Ich kann die Adresse drucken, auf die pzeigt.

if(p)
    cout << p << endl;
  • Ich bin mir zwar sicher, dass wir diesen Zeiger (nach dem letzten Element) nicht dereferenzieren können, wie wir es mit Iteratoren (nach dem letzten Element) nicht können, aber ich bin mir nicht sicher, ob ich diesen Zeiger inkrementiere p? Ist ein undefiniertes Verhalten (UB) wie bei Iteratoren?

    p++; // UB?
Itachi Uchiwa
quelle
4
UB "... Alle anderen Situationen ( dh
Richard Critten
3
Nun, dies ähnelt einem std::vectormit 0 darin enthaltenen Element. begin()ist bereits gleich, end()sodass Sie einen Iterator, der auf den Anfang zeigt, nicht inkrementieren können.
Phil1970
1
@PeterMortensen Ich denke, Ihre Bearbeitung hat die Bedeutung des letzten Satzes geändert ("Was ich mir sicher bin -> Ich bin mir nicht sicher warum"). Könnten Sie das bitte noch einmal überprüfen?
Fabio sagt Reinstate Monica
@PeterMortensen: Der letzte Absatz, den Sie bearbeitet haben, ist etwas weniger lesbar geworden.
Itachi Uchiwa

Antworten:

32

Zeiger auf Elemente von Arrays dürfen auf ein gültiges Element oder auf ein Element nach dem Ende verweisen. Wenn Sie einen Zeiger so erhöhen, dass er über das Ende hinausgeht, ist das Verhalten undefiniert.

Zeigt für Ihr Array der Größe 0 pbereits eins nach dem Ende, sodass das Inkrementieren nicht zulässig ist.

Siehe C ++ 17 8.7 / 4 bezüglich des +Operators ( ++hat die gleichen Einschränkungen):

Wenn der Ausdruck Pauf das Element x[i]eines Array-Objekts xmit n Elementen zeigt, zeigen die Ausdrücke P + Jund J + P(wo Jder Wert ist j) auf das (möglicherweise hypothetische) Element, x[i+j]wenn 0 ≤ i + j ≤ n ist. Andernfalls ist das Verhalten undefiniert.

Interjay
quelle
2
Der einzige Fall x[i]ist also der gleiche wie x[i + j]bei beiden iund jhat den Wert 0?
Rami Yen
8
@RamiYen x[i]ist das gleiche Element wie x[i+j]wenn j==0.
Interjay
1
Ugh, ich hasse die "Twilight Zone" der C ++ - Semantik ... +1.
Einpoklum
4
@ einpoklum-reinstateMonica: Es gibt wirklich keine Dämmerungszone. Es ist nur C ++, das selbst für den Fall N = 0 konsistent ist. Für ein Array von N Elementen gibt es N + 1 gültige Zeigerwerte, da Sie hinter das Array zeigen können. Das bedeutet, dass Sie am Anfang des Arrays beginnen und den Zeiger N-mal erhöhen können, um zum Ende zu gelangen.
MSalters
1
@ MaximEgorushkin Meine Antwort bezieht sich darauf, was die Sprache derzeit erlaubt. Die Diskussion über Sie, die Sie stattdessen zulassen möchten, ist nicht zum Thema.
Interjay
2

Ich denke, Sie haben bereits die Antwort; Wenn Sie etwas genauer hinschauen: Sie haben gesagt, dass das Inkrementieren eines Off-the-End-Iterators UB ist. Diese Antwort lautet: Was ist ein Iterator?

Der Iterator ist nur ein Objekt, das einen Zeiger hat, und das Inkrementieren dieses Iterators erhöht wirklich den Zeiger, den er hat. 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 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 verwenden. 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 seine Adresse zu übernehmen, was 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.

Regentropfen7
quelle
-4

Im strengsten Sinne ist dies kein undefiniertes Verhalten, sondern implementierungsdefiniert. Obwohl dies nicht ratsam ist, wenn Sie Nicht-Mainstream-Architekturen unterstützen möchten, können Sie dies wahrscheinlich tun.

Das von interjay gegebene Standardzitat 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 einer explizit UB, der andere nicht). Es gibt einen Absatz, der sich direkt mit der Operation in der Frage befasst:

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

Oh, warte einen Moment, ein vollständig definierter Objekttyp? Das ist alles? Ich meine wirklich, Typ ? Sie brauchen also überhaupt kein Objekt?
Es erfordert einiges an Lesen, um tatsächlich einen Hinweis darauf zu finden, dass etwas darin möglicherweise nicht ganz so genau definiert ist. Denn bis jetzt liest es sich so, als ob Sie es vollkommen dürfen, ohne Einschränkungen.

[basic.compound] 3gibt eine Aussage darüber ab, welchen Zeigertyp man haben kann, und da keiner der anderen drei ist, würde das Ergebnis Ihrer Operation 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 (z. B. Ende der Speicherdauer) aufgeführt, unter denen Zeiger regelmäßig ungültig werden. Das ist anscheinend eine zulässige Sache. Und in der Tat:

[basic.stc] 4 Die
Indirektion durch einen ungültigen Zeigerwert und die Übergabe eines ungültigen Zeigerwerts an eine Freigabefunktion 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 und daher im Allgemeinen 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 "Zeiger" 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, so wird A [...] auf dieses Objekt verweisen, unabhängig davon, wie der Wert erhalten wurde .
[Hinweis: Beispielsweise wird davon ausgegangen, dass die Adresse nach dem Ende eines Arrays auf ein nicht verwandtes Objekt des Elementtyps des Arrays verweist, das sich möglicherweise an dieser Adresse befindet. [...]].

Lesen Sie als: OK, wen interessiert das? Solange ein Zeiger irgendwo in der Erinnerung zeigt , 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 tatsächlich brauche. Sicher abgeleitet. Anscheinend kann ich immer noch nicht sicher abgeleitete Zeiger haben. Ich vermute, dass eine Dereferenzierung wahrscheinlich keine so gute Idee wäre, aber es ist durchaus zulässig, sie zu haben. Es sagt nichts anderes.

Eine Implementierung kann die Zeigersicherheit gelockert haben. 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 strenge 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 als erreichbar deklariert

Warten Sie, also ist es sogar möglich, dass ich declare_reachable()jeden Zeiger aufrufen muss? Wie soll ich wissen?

Jetzt können Sie in konvertieren intptr_t, was genau definiert ist und eine ganzzahlige Darstellung eines sicher abgeleiteten Zeigers liefert. Da es sich natürlich um eine Ganzzahl handelt, ist es absolut legitim und klar definiert, sie nach Belieben zu erhöhen.
Und ja, Sie können den intptr_tRücken in einen Zeiger konvertieren , der ebenfalls gut definiert ist. Da dies nicht der ursprüngliche Wert ist, kann nicht mehr garantiert werden, dass Sie (offensichtlich) über einen sicher abgeleiteten Zeiger verfügen. Alles in allem ist dies jedoch, wie in der Implementierung definiert, nach dem Buchstaben des Standards eine zu 100% legitime Sache:

[expr.reinterpret.cast] 5
Ein Wert vom Integraltyp oder Aufzählungstyp kann explizit in einen Zeiger konvertiert werden. Ein Zeiger, der in eine Ganzzahl von ausreichender Größe [...] und zurück zum gleichen [...] ursprünglichen Wert des Zeigertyps konvertiert wurde; Zuordnungen zwischen Zeigern und Ganzzahlen sind ansonsten implementierungsdefiniert.

Der Fang

Zeiger sind nur gewöhnliche ganze Zahlen, 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 Generieren eines ungültigen Zeigers (nicht dereferenzieren, nur in einem Zeigerregister haben) führt zu einer Falle.

Das ist also die Basis für "Implementierung definiert". Das, und die Tatsache , dass ein Zeiger erhöht wird, wann immer Sie wollen, als Sie bitte könnte natürlich Ursache Überlauf, der die Norm nicht behandeln will. Der Adressraum am Ende der Anwendung fällt möglicherweise nicht mit dem Ort des Überlaufs zusammen, und Sie wissen nicht einmal, ob es einen Überlauf für Zeiger auf eine bestimmte Architektur gibt. Alles in allem ist es ein albtraumhaftes Durcheinander, das in keiner Beziehung zu den möglichen Vorteilen steht.

Der Umgang mit der One-Past-Object-Bedingung auf der anderen Seite ist einfach: Die Implementierung muss einfach 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.

Damon
quelle
1
Ihre Logik ist fehlerhaft. "Also brauchst du überhaupt kein Objekt?" interpretiert den Standard falsch, indem er sich auf eine einzelne Regel konzentriert. Bei dieser Regel geht es um die Kompilierungszeit, unabhängig davon, ob Ihr Programm gut geformt ist. Es gibt eine andere Regel zur Laufzeit. Nur zur Laufzeit können Sie tatsächlich über die Existenz von Objekten an einer bestimmten Adresse sprechen. Ihr Programm muss alle Regeln erfüllen . die Kompilierungszeitregeln zur Kompilierungszeit und die Laufzeitregeln zur Laufzeit.
MSalters
5
Sie haben ähnliche logische Fehler mit "OK, wen interessiert das! Solange ein Zeiger irgendwo im Gedächtnis zeigt, bin ich gut?". Nein, Sie müssen alle Regeln befolgen. Die schwierige Sprache über "Ende eines Arrays ist Beginn eines anderen Arrays" gibt der Implementierung nur die Berechtigung, Speicher zusammenhängend zuzuweisen. Zwischen den Zuweisungen muss kein freier Speicherplatz vorhanden sein. Das bedeutet, dass Ihr Code möglicherweise den gleichen Wert A hat wie das Ende eines Array-Objekts und der Anfang eines anderen.
MSalters
1
"Eine Falle" kann nicht durch "implementierungsdefiniertes" Verhalten beschrieben werden. Beachten Sie, dass interjay die Einschränkung für den +Operator gefunden hat (von dem aus ++fließt), was bedeutet, dass das Zeigen nach "one-after-the-end" undefiniert ist.
Martin Bonner unterstützt Monica
1
@PeterCordes: Bitte lesen Sie basic.stc, Absatz 4 . Es heißt "Indirektion [...] undefiniertes Verhalten. Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten" . Ich verwirre die Leute nicht, indem ich diesen Begriff für eine andere Bedeutung verwende. Es ist der genaue Wortlaut. Es ist kein undefiniertes Verhalten.
Damon
2
Es ist kaum möglich, dass Sie eine Lücke für das Nachinkrement gefunden haben, aber Sie zitieren nicht den vollständigen Abschnitt darüber, was das Nachinkrement bewirkt. Ich werde mich jetzt nicht selbst darum kümmern. Einverstanden, dass wenn es eine gibt, es nicht beabsichtigt ist. So schön es auch wäre, wenn ISO C ++ mehr Dinge für Flat-Memory-Modelle definieren würde, @MaximEgorushkin, es gibt andere Gründe (wie das Umschließen von Zeigern), um willkürliche Dinge nicht zuzulassen. Siehe Kommentare zu Sollten Zeigervergleiche in 64-Bit x86 signiert oder nicht signiert sein?
Peter Cordes