Frage Ist ein Zeiger mit der richtigen Adresse und dem Typ immer noch ein gültiger Zeiger seit C ++ 17?


(In Bezug auf diese Frage und Antwort.)

Vor dem C ++ 17 Standard wurde der folgende Satz in enthalten [basic.compound] / 3:

Wenn sich ein Objekt vom Typ T an einer Adresse A befindet, wird gesagt, dass ein Zeiger vom Typ cvT *, dessen Wert die Adresse A ist, auf dieses Objekt zeigt, unabhängig davon, wie der Wert erhalten wurde.

Aber seit C ++ 17 ist dieser Satz gewesen entfernt.

Zum Beispiel glaube ich, dass dieser Satz diesen Beispielcode definiert hat, und dass seit C ++ 17 dies undefiniertes Verhalten ist:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

Vor C ++ 17, p1+1 hält die Adresse an *p2 und hat den richtigen Typ, also *(p1+1) ist ein Zeiger auf *p2. In C ++ 17 p1+1 ist ein Zeiger über das Ende hinausAlso ist es kein Zeiger auf Objekt und ich glaube, es ist nicht dereferenzierbar.

Ist diese Interpretation dieser Modifikation des Standards richtig oder gibt es andere Regeln, die die Streichung des zitierten Satzes kompensieren?


76
2018-01-02 14:00


Ursprung


Antworten:


Ist diese Interpretation dieser Modifikation des Standards richtig oder gibt es andere Regeln, die die Streichung dieses zitierten Satzes kompensieren?

Ja, diese Interpretation ist richtig. Ein Zeiger nach dem Ende ist nicht einfach in einen anderen Zeigerwert umwandelbar, der zufällig auf diese Adresse zeigt.

Das neue [basic.compound] / 3 sagt:

Jeder Wert des Zeigertyps ist einer der folgenden:
  (3.1)   ein Zeiger auf ein Objekt oder eine Funktion (der Zeiger soll auf das Objekt oder die Funktion zeigen), oder
  (3.2)   ein Zeiger hinter dem Ende eines Objekts ([expr.add]), oder

Diese schließen sich gegenseitig aus. p1+1 ist ein Zeiger nach dem Ende, kein Zeiger auf ein Objekt. p1+1 weist auf eine hypothetische x[1] eines Arrays der Größe 1 bei p1nicht zu p2. Diese beiden Objekte sind nicht ineinander übersetzbar.

Wir haben auch die nicht normative Notiz:

[Hinweis: Ein Zeiger hinter dem Ende eines Objekts ([expr.add]) wird nicht als auf ein Objekt des Objekttyps nicht bezogen betrachtet, das sich möglicherweise an dieser Adresse befindet. [...]

was die Absicht verdeutlicht.


Als T.C. weist auf zahlreiche Kommentare hin (vor allem dieser), das ist wirklich ein spezieller Fall des Problems, das mit dem Versuch zu implementieren kommt std::vector - welches ist das [v.data(), v.data() + v.size()) muss ein gültiger Bereich sein und doch vector erzeugt kein Array-Objekt, so dass die einzige definierte Zeigerarithmetik von einem beliebigen gegebenen Objekt in dem Vektor zu einem Ende des hypothetischen Arrays mit einer Größe gehen würde. Für mehr Ressourcen, siehe CWG 2182, Diese erste Diskussionund zwei Überarbeitungen eines Papiers zu diesem Thema: P0593R0 und P0593R1 (Abschnitt 1.3 speziell).


43
2018-01-02 14:14



In Ihrem Beispiel *(p1 + 1) = 10; sollte UB sein, weil es ist eine nach dem Ende des Arrays Größe 1. Aber wir sind hier in einem sehr speziellen Fall, weil das Array dynamisch in einem größeren Char-Array konstruiert wurde.

Die dynamische Objekterstellung wird in beschrieben 4.5 Das C ++ Objektmodell [intro.object], §3 des n4659-Entwurfs des C ++ - Standards:

3 Wenn ein vollständiges Objekt (8.3.4) im Speicher erstellt wird, das einem anderen Objekt e vom Typ "Array of N   unsigned char "oder des Typs" array of N std :: byte "(21.2.1), dieses Array bietet Speicherplatz für das erstellte   Objekt wenn:
  (3.1) - die Lebenszeit von e hat begonnen und nicht beendet, und
  (3.2) - Der Speicher für das neue Objekt passt vollständig in e, und
  (3.3) - Es gibt kein kleineres Array-Objekt, das diese Einschränkungen erfüllt.

Das 3.3 scheint eher unklar, aber die folgenden Beispiele verdeutlichen die Absicht:

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

Also im Beispiel, der buffer Array bietet Speicherplatz für beide *p1 und *p2.

Die folgenden Absätze beweisen, dass das vollständige Objekt für beide *p1 und *p2 ist buffer:

4 Ein Objekt a ist innerhalb eines anderen Objekts b verschachtelt, wenn:
  (4.1) - a ist ein Unterobjekt von b, oder
  (4.2) - b bietet Speicherplatz für ein, oder
  (4.3) - Es gibt ein Objekt c, in dem a in c verschachtelt ist und c in b verschachtelt ist.

5 Für jedes Objekt x gibt es ein Objekt namens vollständiges Objekt von x, das wie folgt bestimmt wird:
  (5.1) - Wenn x ein vollständiges Objekt ist, dann ist das vollständige Objekt von x selbst.
  (5.2) - Ansonsten ist das vollständige Objekt von x das vollständige Objekt des (eindeutigen) Objekts, das x enthält.

Sobald dies festgestellt ist, ist der andere relevante Teil des Entwurfs n4659 für C ++ 17 [basic.coampound] §3 (betone meinen):

3 ... Jeder   Der Wert des Zeigertyps ist einer der folgenden:
  (3.1) - ein Zeiger auf ein Objekt oder eine Funktion (der Zeiger soll auf das Objekt oder die Funktion zeigen), oder
  (3.2) - ein Zeiger nach dem Ende eines Objekts (8.7), oder
  (3.3) - der Nullzeigerwert (7.11) für diesen Typ, oder
  (3.4) - ein ungültiger Zeigerwert.

Ein Wert eines Zeigertyps, bei dem es sich um einen Zeiger auf oder über das Ende eines Objekts hinaus handelt, entspricht der Adresse des Objekts   erstes Byte im Speicher (4.4), das vom Objekt belegt ist oder das erste Byte im Speicher nach dem Ende des Speichers   durch das Objekt jeweils besetzt. [Hinweis: Ein Zeiger hinter dem Ende eines Objekts (8.7) wird nicht berücksichtigt   zeigen Sie auf ein nicht verwandt Objekt des Objekttyps, das sich möglicherweise an dieser Adresse befindet. Ein Zeigerwert   wird ungültig, wenn der von ihm angegebene Speicher das Ende seiner Speicherdauer erreicht; siehe 6.7. Endnote]   Für Zeigerarithmetik (8.7) und Vergleich (8.9, 8.10) ein Zeiger nach dem Ende des letzten Elements   eines Arrays x von n Elementen wird als äquivalent zu einem Zeiger auf ein hypothetisches Element x [n] betrachtet. Das   Wertdarstellung von Zeigertypen ist implementierungsdefiniert. Zeiger zu Layout-kompatiblen Typen sollen   haben die gleichen Wert Darstellung und Ausrichtung Anforderungen (6.11) ...

Die Notiz Ein Zeiger über das Ende hinaus ... trifft hier nicht zu, weil die Objekte, auf die gezeigt wird p1 und p2und nicht nicht verwandt, sind aber in dasselbe vollständige Objekt verschachtelt, so dass Zeigerarithmetik innerhalb des Objekts, das Speicher bereitstellt, sinnvoll ist: p2 - p1 ist definiert und ist (&buffer[sizeof(int)] - buffer]) / sizeof(int) das ist 1.

Damit p1 + 1  ist ein Zeiger auf *p2, und *(p1 + 1) = 10; hat Verhalten definiert und setzt den Wert von *p2.


Ich habe auch den Anhang C4 über die Kompatibilität zwischen C ++ 14 und aktuellen (C ++ 17) Standards gelesen. Das Entfernen der Möglichkeit, Zeigerarithmetik zwischen Objekten zu verwenden, die dynamisch in einem einzelnen Zeichenfeld erzeugt werden, wäre eine wichtige Änderung, die IMHO dort zitiert werden sollte, da es ein allgemein verwendetes Merkmal ist. Da nichts davon in den Kompatibilitätsseiten existiert, glaube ich, dass es bestätigt, dass es nicht die Absicht des Standards war, es zu verbieten.

Insbesondere würde es die übliche dynamische Konstruktion eines Arrays von Objekten aus einer Klasse ohne Standardkonstruktor zunichte machen:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr kann dann als Zeiger auf das erste Element eines Arrays verwendet werden ...


8
2018-01-02 17:40



Die hier gegebenen Antworten zu erweitern, ist ein Beispiel dafür, was meiner Meinung nach die überarbeitete Formulierung ausschließt:

Warnung: Undefiniertes Verhalten

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

Für vollständig implementierungsabhängige (und fragile) Gründe ist die mögliche Ausgabe dieses Programms:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Diese Ausgabe zeigt, dass die beiden Arrays (in diesem Fall) zufällig im Speicher gespeichert sind, so dass "eins nach dem Ende" von A hält den Wert der Adresse des ersten Elements von B.

Die überarbeitete Spezifikation stellt dies unabhängig davon sicher A+1 ist nie ein gültiger Zeiger auf B. Der alte Satz "unabhängig davon, wie der Wert erhalten wird" besagt, dass, wenn "A + 1" zufällig auf "B [0]" zeigt, es ein gültiger Zeiger auf "B [0]" ist. Das kann nicht gut sein und sicherlich niemals die Absicht.


0
2018-01-03 11:23