web-dev-qa-db-de.com

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

(In Bezug auf diese Frage und Antwort .)

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

Befindet sich ein Objekt vom Typ T an einer Adresse A, zeigt ein Zeiger vom Typ cv T *, dessen Wert die Adresse A ist, auf dieses Objekt, unabhängig davon, wie der Wert erhalten wurde.

Aber seit C++ 17 ist dieser Satz entfernt .

Zum Beispiel glaube ich, dass dieser Satz diesen Beispielcode definiert hat und dass dies seit C++ 17 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 enthält p1+1 Die Adresse von *p2 Und hat den richtigen Typ. Daher ist *(p1+1) ein Zeiger auf *p2. In C++ 17 ist p1+1 Ein Zeiger nach dem Ende , also kein Zeiger auf Objekt und ich glaube, es ist nicht dereferenzierbar.

Ist diese Auslegung dieser Änderung des Standardrechts oder gibt es andere Regeln, die die Streichung des zitierten Satzes kompensieren?

80
Oliv

Ist diese Auslegung dieser Änderung des Standardrechts oder gibt es andere Regeln, die die Streichung dieses zitierten Satzes kompensieren?

Ja, diese Interpretation ist richtig. Ein Zeiger hinter dem Ende kann nicht einfach in einen anderen Zeigerwert konvertiert werden, der zufällig auf diese Adresse verweist.

Das neue [basic.compound]/ 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) einen 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 Zeigt auf ein hypothetisches x[1] Eines Arrays der Größe 1 bei p1, Nicht auf p2. Diese beiden Objekte sind nicht Zeiger-interkonvertierbar.

Wir haben auch die nicht normative Anmerkung:

[Hinweis: Ein Zeiger hinter dem Ende eines Objekts ([Ausdruck]) verweist nicht auf ein nicht verwandtes Objekt des Objekttyps, das sich möglicherweise an dieser Adresse befindet. [...]

das klärt die absicht.


Als T.C. weist in zahlreichen Kommentaren darauf hin ( insbesondere dieses ), dass dies wirklich ein Sonderfall des Problems ist, das mit dem Versuch verbunden ist, std::vector zu implementieren - das ist [v.data(), v.data() + v.size()) muss ein gültiger Bereich sein, und dennoch erstellt vector kein Array-Objekt, sodass die einzige definierte Zeigerarithmetik von einem bestimmten Objekt im Vektor zum Ende seiner hypothetischen Einheitsgröße führt Array. Weitere Ressourcen finden Sie unter CWG 2182 , diese Standarddiskussion und zwei Überarbeitungen eines Papiers zu diesem Thema: P0593R und P0593R1 (speziell Abschnitt 1.3).

44
Barry

In Ihrem Beispiel sollte *(p1 + 1) = 10; UB sein, da es eins nach dem Ende des Arrays der Größe 1 ist. Aber wir sind es in einem ganz besonderen Fall hier, weil das Array dynamisch in einem größeren char-Array aufgebaut wurde.

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

3 Wenn ein vollständiges Objekt im Speicher erstellt wird (8.3.4), das einem anderen Objekt e vom Typ "Array mit N vorzeichenlosen Zeichen" oder vom Typ "Array mit N std :: byte" (21.2.1) zugeordnet ist, stellt dieses Array Speicher bereit für das erstellte Objekt, wenn:
(3.1) - die Lebenszeit von e hat begonnen und nicht geendet, 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 Bedingungen erfüllt.

Der 3.3 scheint eher unklar, aber die folgenden Beispiele machen die Absicht klarer:

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)

In diesem Beispiel bietet das Array buffer Speicherplatz für *p1 Und *p2.

Die folgenden Absätze beweisen, dass das vollständige Objekt für *p1 Und *p2buffer ist:

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

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

Sobald dies festgelegt ist, ist der andere relevante Teil des Entwurfs n4659 für C++ 17 [basic.coumpound] §3 (betonen Sie meinen):

3 ... 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 (8.7) oder
(3.3) - der Nullzeigerwert (7.11) für diesen Typ, oder
(3.4) - ein ungültiger Zeigerwert.

Ein Wert eines Zeigertyps, der ein Zeiger auf oder nach dem Ende eines Objekts ist, stellt die Adresse des ersten Bytes im Speicher (4.4) dar, das vom Objekt belegt wird oder das erste Byte im Speicher nach dem Ende des Speichers vom Objekt belegt. [Hinweis: Ein Zeiger nach dem Ende eines Objekts (8.7) zeigt nicht auf ein nicht verwandtes 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. —End note] Zum Zwecke der Zeigerarithmetik (8.7) und des Vergleichs (8.9, 8.10) wird ein Zeiger nach dem Ende des letzten Elements eines Arrays x von n Elementen als äquivalent zu einem Zeiger auf ein hypothetisches Element x [angesehen. n]. Die Wertdarstellung von Zeigertypen ist implementierungsdefiniert. Zeiger auf layoutkompatible Typen müssen die gleichen Anforderungen an die Wertedarstellung und Ausrichtung haben (6.11) ...

Der Hinweis Ein Zeiger nach dem Ende ... entfällt hier, da die Objekte mit p1 Und p2 und nicht unabhängig , sondern im selben vollständigen Objekt verschachtelt, sodass Zeigerarithmetik innerhalb des Objekts, das Speicherplatz bietet, Sinn macht: p2 - p1 ist definiert und ist (&buffer[sizeof(int)] - buffer]) / sizeof(int) das ist 1.

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


Ich habe auch den C4-Anhang ü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 Zeichenarray erstellt wurden, wäre eine wichtige Änderung, die IMHO dort zitieren sollte, da dies eine häufig verwendete Funktion ist. Da auf den Kompatibilitätsseiten nichts darüber vorhanden ist, wird meines Erachtens bestätigt, dass es nicht die Absicht des Standards war, dies zu verbieten.

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

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
Serge Ballesta

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;
}

Aus vollständig implementierungsabhängigen (und fragilen) Gründen ist die Ausgabe dieses Programms möglich:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

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

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

1
Persixty