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

84

(In Bezug auf diese Frage und Antwort .)

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

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

Aber seit C ++ 17 wurde dieser Satz entfernt .

Zum Beispiel glaube ich, dass dieser Satz diesen Beispielcode definiert hat und dass dies seit C ++ 17 ein 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;

Hält vor C ++ 17 p1+1die Adresse an *p2und hat den richtigen Typ, also *(p1+1)einen Zeiger auf *p2. In C ++ 17 p1+1ist es ein Zeiger hinter dem Ende , also kein Zeiger auf ein Objekt, und ich glaube, es ist nicht dereferenzierbar.

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

Oliv
quelle
Hinweis: Es gibt neue / aktualisierte Regeln zur Zeigerherkunft in [basic.stc.dynamic.safety] und [util.dynamic.safety]
MM
@MM Dies ist nur bei Implementierungen mit strenger Zeigersicherheit von Bedeutung, bei denen es sich um eine leere Menge handelt (innerhalb des experimentellen Fehlers).
TC
4
Die zitierte Aussage hat sich in der Praxis nie bewahrheitet. Gegeben int a, b = 0;, Sie können nicht einmal tun *(&a + 1) = 1;, wenn Sie überprüft haben &a + 1 == &b. Wenn Sie einen gültigen Zeiger auf ein Objekt erhalten können, indem Sie nur dessen Adresse erraten, wird das Speichern lokaler Variablen in Registern problematisch.
TC
@TC 1) Welcher Compiler fügt eine Variable in reg ein, nachdem Sie ihre Adresse übernommen haben? 2) Wie erraten Sie eine Adresse richtig, ohne sie zu messen?
Neugieriger
@curiousguy Genau deshalb ist es problematisch, einfach eine Zahl, die auf andere Weise erhalten wurde (z. B. durch Raten), an die Adresse zu übertragen, an der sich ein Objekt befindet: Es aliasisiert dieses Objekt, aber der Compiler ist sich dessen nicht bewusst. Wenn Sie dagegen die Adresse des Objekts verwenden, ist dies wie gesagt: Der Compiler wird gewarnt und synchronisiert sich entsprechend.
Peter - Stellen Sie Monica am

Antworten:

45

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

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

Die 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 nach dem Ende eines Objekts ([Ausdruck .hinzufügen oder

Diese schließen sich gegenseitig aus. p1+1ist ein Zeiger hinter dem Ende, kein Zeiger auf ein Objekt. p1+1zeigt auf eine Hypothese x[1]eines Arrays der Größe 1 bei p1, nicht auf p2. Diese beiden Objekte sind nicht zeigerinterkonvertierbar.

Wir haben auch den nicht normativen Hinweis:

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

das klärt die Absicht.


Wie TC in zahlreichen Kommentaren ( insbesondere in diesem ) hervorhebt , ist dies wirklich ein Sonderfall des Problems, das mit dem Implementierungsversuch einhergeht std::vector- das heißt, das [v.data(), v.data() + v.size())muss ein gültiger Bereich sein und dennoch vectorkein Array-Objekt erstellen Nur eine definierte Zeigerarithmetik würde von einem bestimmten Objekt im Vektor bis zum Ende seines hypothetischen Arrays mit einer Größe gehen. Weitere Ressourcen finden Sie in CWG 2182 , dieser Standarddiskussion und zwei Überarbeitungen eines Papiers zu diesem Thema: P0593R0 und P0593R1 (speziell Abschnitt 1.3).

Barry
quelle
3
Dieses Beispiel ist im Grunde ein Sonderfall des bekannten " vectorImplementierbarkeitsproblems". +1.
TC
2
@Oliv Der allgemeine Fall existiert seit C ++ 03. Die Hauptursache ist, dass die Zeigerarithmetik nicht wie erwartet funktioniert, da Sie kein Array-Objekt haben.
TC
1
@TC Ich glaubte, das einzige Problem sei die Einschränkung der Zeigerarithmetik. Fügt diese Satzlöschung nicht ein neues Problem hinzu? Ist das Codebeispiel auch UB in Pre-C ++ 17?
Oliv
1
@Oliv Wenn die Zeigerarithmetik festgelegt ist, wird p1+1kein Zeiger über das Ende hinaus erstellt, und die gesamte Diskussion über Zeiger nach dem Ende ist umstritten. Ihr spezieller Zwei-Elemente-Sonderfall ist möglicherweise nicht UB vor 17, aber auch nicht sehr interessant.
TC
5
@TC Können Sie mich auf eine Stelle hinweisen, an der ich mich über dieses "Problem der Vektorimplementierbarkeit" informieren kann?
SirGuy
8

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

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

3 Wenn ein vollständiges Objekt (8.3.4) im Speicher erstellt wird, der einem anderen Objekt e vom Typ "Array von N vorzeichenlosem Zeichen" oder vom Typ "Array von N std :: byte" (21.2.1) zugeordnet ist, stellt dieses Array Speicher bereit für das erstellte Objekt, wenn:
(3.1) - die Lebensdauer von e begonnen und nicht beendet wurde und
(3.2) - der Speicher für das neue Objekt vollständig in e passt und
(3.3) - es kein kleineres Array-Objekt gibt, das diese erfüllt Einschränkungen.

Die 3.3 scheint ziemlich 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)

Im Beispiel bietet das bufferArray Speicher für *p1und *p2.

Die folgenden Absätze beweisen, dass das vollständige Objekt für beide *p1und *p2ist buffer:

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

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, 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 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, der ein Zeiger auf oder hinter das Ende eines Objekts ist, repräsentiert die Adresse des ersten Bytes im Speicher (4.4), das vom Objekt belegt ist, oder des ersten Bytes im Speicher nach dem Ende des vom Objekt belegten Speichers , beziehungsweise. [Hinweis: Ein Zeiger hinter dem Ende eines Objekts (8.7) zeigt nicht auf ein nicht verwandtes ObjektObjekt 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] Für die Zeigerarithmetik (8.7) und den Vergleich (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 dieselben Anforderungen an die Wertdarstellung und Ausrichtung haben (6.11) ...

Der Hinweis Ein Zeiger nach dem Ende ... gilt hier nicht, da die Objekte, auf die durch p1und p2nicht unabhängig verwiesen wird, in dasselbe vollständige Objekt verschachtelt sind, sodass die Zeigerarithmetik innerhalb des Objekts, das Speicher bereitstellt, sinnvoll ist: p2 - p1definiert ist und ist (&buffer[sizeof(int)] - buffer]) / sizeof(int)das ist 1.

Ist p1 + 1 also ein Zeiger auf *p2und *(p1 + 1) = 10;hat 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, bestätigt dies meines Erachtens, dass es nicht die Absicht des Standards war, dies zu verbieten.

Insbesondere würde diese allgemeine dynamische Konstruktion eines Arrays von Objekten aus einer Klasse ohne Standardkonstruktor zunichte gemacht:

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 ...

Serge Ballesta
quelle
Aha, die Welt ist also nicht verrückt geworden. +1
Geschichtenerzähler - Unslander Monica
@ StoryTeller: Ich hoffe auch. Außerdem kein Wort dazu im Kompatibilitätsbereich. Aber es sieht so aus, als ob die gegenteilige Meinung hier mehr Ruf hat ...
Serge Ballesta
2
Sie ergreifen ein einzelnes Wort, "nicht verwandt", in einer nicht normativen Notiz und geben ihm eine Bedeutung, die es nicht tragen kann, im Widerspruch zu den normativen Regeln in [expr.add], die die Zeigerarithmetik regeln. Anhang C enthält nichts, da die Zeigerarithmetik im allgemeinen Fall in keinem Standard funktioniert hat. Es gibt nichts zu brechen.
TC
3
@TC: Google ist sehr wenig hilfreich beim Auffinden von Informationen zu diesem "Vektorimplementierbarkeitsproblem". Könnten Sie helfen?
Matthieu M.
6
@MatthieuM. Siehe Kernthema 2182 , die std-Diskussion, P0593R0 und P0593R1 (insbesondere Abschnitt 1.3) . Das Grundproblem besteht darin, dass vectorkein Array-Objekt erstellt wird (und nicht erstellt werden kann), sondern über eine Schnittstelle verfügt, über die der Benutzer einen Zeiger abrufen kann, der die Zeigerarithmetik unterstützt (die nur für Zeiger auf Array-Objekte definiert ist).
TC
1

Die hier gegebenen Antworten zu erweitern, ist ein Beispiel dafür, was meiner Meinung nach der überarbeitete Wortlaut 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 mögliche Ausgabe dieses Programms:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

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

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

Persixty
quelle
Verbietet dies auch effektiv die Verwendung eines leeren Arrays am Ende einer Struktur, sodass eine abgeleitete Klasse oder ein neuer benutzerdefinierter Allokator ein Array mit benutzerdefinierter Größe angeben kann? Vielleicht ist das neue Problem das "unabhängig davon wie" - es gibt einige Wege, die gültig sind, und einige Wege, die gefährlich sind?
Gem Taylor
@Persixty Der Wert eines Zeigerobjekts wird also durch die Bytes der Objekte bestimmt und sonst nichts. Zwei Objekte mit demselben Status zeigen also auf dasselbe Objekt. Wenn einer gültig ist, ist der andere auch gültig. Bei gängigen Architekturen, bei denen ein Zeigerwert als Zahl dargestellt wird, zeigen zwei Zeiger mit gleichen Werten auf dieselben Objekte und einer am Ende auf dieselben anderen Objekte.
Neugieriger
@Persixty Trivialer Typ bedeutet auch, dass Sie die möglichen Werte eines Typs auflisten können. Grundsätzlich betrachtet jeder moderne Compiler in einem Optimierungsmodus (selbst -O0bei einigen Compilern) Zeiger nicht als triviale Typen. Compiler nehmen die Anforderungen des Standards nicht ernst und auch nicht die Leute, die den Standard schreiben, von einer anderen Sprache träumen und alle Arten von Erfindungen machen, die den Grundprinzipien direkt widersprechen. Offensichtlich sind Benutzer verwirrt und werden manchmal schlecht behandelt, wenn sie sich über Compiler-Fehler beschweren.
Neugieriger
Die nicht normative Anmerkung in der Frage möchte, dass wir uns "One-Past-The-End" so vorstellen, dass es auf nichts hinweist. Wir wissen beide, dass es in der Praxis durchaus möglich ist, auf etwas hinzuweisen, und in der Praxis kann es möglich sein, es zu dereferenzieren. Dies ist jedoch (gemäß Standard) kein gültiges Programm. Wir können uns eine Implementierung vorstellen , die weiß, dass ein Zeiger durch Arithmetik bis zum Ende erhalten wurde und eine Ausnahme auslöst, wenn sie dereferenziert wird. Ich kenne zwar eine Plattform, die das tut. Ich denke, der Standard will es nicht ausschließen.
Persixty
@curiousguy Ich bin mir auch nicht sicher, was Sie unter Aufzählung der möglichen Werte verstehen. Dies ist keine erforderliche Funktion eines trivialen Typs im Sinne von C ++.
Persixty