web-dev-qa-db-de.com

Darf delete seinen Parameter ändern?

In einer Antwort https://stackoverflow.com/a/704568/8157187 steht ein Zitat von Stroustrup:

C++ erlaubt explizit eine Implementierung von delete, um einen lvalue-Operanden auf Null zu setzen, und ich hatte gehofft, dass Implementierungen dies tun würden, aber diese Idee scheint bei Implementierern nicht populär geworden zu sein.

Ich konnte diese explizite Aussage jedoch nicht im Standard finden. Es gibt einen Teil des aktuellen Normentwurfs (N4659), den man folgendermaßen interpretieren kann:

6.7:

Wenn das Ende der Dauer eines Speicherbereichs erreicht ist, werden die Werte aller Zeiger, die die Adresse eines Teils dieses Speicherbereichs darstellen, zu ungültigen Zeigerwerten (6.9.2). Die Indirektion durch einen ungültigen Zeigerwert und die Übergabe eines ungültigen Zeigerwerts an eine Aufhebungsfunktion haben ein nicht definiertes Verhalten. Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten.

Fußnote: Einige Implementierungen definieren möglicherweise, dass das Kopieren eines ungültigen Zeigerwerts einen vom System generierten Laufzeitfehler verursacht

Nach einem delete ptr; wird der Wert von ptr zu einem ungültigen Zeigerwert, und die Verwendung dieses Werts hat ein implementierungsdefiniertes Verhalten. Es heißt jedoch nicht, dass sich der Wert von ptr ändern darf.

Es mag eine philosophische Frage sein, wie man entscheiden kann, dass sich ein Wert geändert hat, wenn man seinen Wert nicht verwenden kann.

6.9:

Unabhängig davon, ob das Objekt einen gültigen Wert vom Typ T enthält oder nicht, können für jedes Objekt (mit Ausnahme eines Basisklassen-Unterobjekts) des einfach kopierbaren Typs T die zugrunde liegenden Bytes (4.4), aus denen das Objekt besteht, in ein Array von Zeichen kopiert werden. unsigned char oder std :: byte (21.2.1) .43 Wenn der Inhalt dieses Arrays zurück in das Objekt kopiert wird, behält das Objekt anschließend seinen ursprünglichen Wert.

Es scheint also, dass es für memcpy einen ungültigen Zeigerwert in ein char-Array gibt (abhängig davon, welche Aussage "stärker" ist, 6.7 oder 6.9. Für mich scheint 6.9 stärker zu sein).

Auf diese Weise kann ich feststellen, dass der Zeigerwert durch delete geändert wurde: memcpy der Wert des Zeigers vor und nach dem Array delete to char, und diese dann vergleichen.

So wie ich es verstehe, gewährt 6.7 nicht, dass delete seinen Parameter ändern darf.

Darf delete seinen Parameter ändern?

Schauen Sie sich die Kommentare hier an: https://stackoverflow.com/a/45142972/8157187


Hier ist ein unwahrscheinlicher, aber immer noch möglicher Code für die reale Welt:

SomeObject *o = ...; // We have a SomeObject
// This SomeObject is registered into someHashtable, with its memory address
// The hashtable interface is C-like, it handles opaque keys (variable length unsigned char arrays)

delete o;

unsigned char key[sizeof(o)];
memcpy(key, &o, sizeof(o)); // Is this line OK? Is its behavior implementation defined?
someHashtable.remove(key, sizeof(key)); // Remove o from the hashtable

Natürlich kann dieses Snippet nachbestellt werden, so dass es zu einem sicher gültigen Code wird. Aber die Frage ist: Ist das ein gültiger Code?


Hier ist ein verwandter Gedankengang: Angenommen, eine Implementierung definiert, was Fußnote beschreibt:

das Kopieren eines ungültigen Zeigerwerts verursacht einen vom System generierten Laufzeitfehler

6.9 garantiert, dass ich irgendeinen Wert memcpy() kann. Sogar eine ungültige. Wenn ich in dieser theoretischen Implementierung memcpy() den ungültigen Zeigerwert (der erfolgreich sein sollte, 6.9 garantiert dies), verwende ich in gewisser Weise nicht den ungültigen Zeigerwert, sondern nur die zugrunde liegenden Bytes (da dies einen Laufzeitfehler verursachen würde) , und 6.9 erlaubt es nicht), also gilt 6.7 nicht .

36
geza

Vor dem Löschen war der Wert von ptr gültig. Nach dem Löschen war der Wert ungültig. Daher hat sich der Wert geändert. Gültige Werte und ungültige Werte schließen sich gegenseitig aus - ein Wert kann nicht gleichzeitig gültig und ungültig sein.

Ihre Frage hat ein grundlegendes Missverständnis. Sie kombinieren diese zwei verschiedenen Konzepte:

  • Der Wert einer Variablen
  • Die Darstellung einer Variablen im Speicher.

Es gibt keine Eins-zu-Eins-Entsprechung zwischen diesen beiden Dingen. Derselbe Wert kann mehrere Darstellungen haben, und dieselbe Darstellung kann verschiedenen Werten entsprechen. 


Ich denke, der Kern Ihrer Frage ist: Kann delete ptr; die Darstellung von ptr ändern? . Darauf lautet die Antwort "Ja". Sie können den gelöschten Zeiger in einem char-Array speichern, die Bytes untersuchen und feststellen, dass alle Bytes mit dem Wert Null sind (oder etwas anderes). Dies wird im Standard von C++ 14 [basic.stc.dynamic.deallocation]/4 (oder C++ 17 [basic.stc]/4) behandelt:

Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten. 

Es ist implementierungsdefiniert und die Implementierung könnte definieren, dass das Untersuchen der Bytes Bytes mit dem Wert Null ergibt.


Ihr Code-Snippet basiert auf dem von der Implementierung definierten Verhalten. "Gültiger Code" ist keine vom Standard verwendete Terminologie, der beabsichtigte Eintrag wird jedoch möglicherweise nicht aus der Hashtabelle entfernt.

Wie von Stroustrup angedeutet, ist dies eine beabsichtigte Designentscheidung. Ein Beispiel wäre ein Compiler im Debug-Modus, der gelöschte Zeiger auf eine bestimmte Repräsentation setzt, sodass ein Laufzeitfehler auftreten kann, wenn ein gelöschter Zeiger anschließend verwendet wird. Hier ist ein Beispiel dieses Prinzips für uninitialisierte Zeiger.

Historischer Hinweis: In C++ 11 war dieser Fall undefined und nicht implementierungsdefiniert. Das Verhalten bei der Verwendung eines gelöschten Zeigers war also identisch mit dem Verhalten bei der Verwendung eines nicht initialisierten Zeigers. In der C-Sprache wird als Freigabespeicher definiert, dass alle Zeiger auf diesen Speicher in den gleichen Status versetzt werden wie ein nicht initialisierter Zeiger.

12
M.M

Den Kontext, in dem Sie die Anweisung von Stroustrup gefunden haben, finden Sie unter Stroustrup, Null löschen

Stroustrup lässt dich überlegen

delete p;
// ...
delete p;

Nach dem ersten Löschen war der Zeiger p ungültig. Das zweite Löschen ist falsch, aber es hat keine Auswirkungen, wenn p nach dem ersten Löschen auf 0 gesetzt wurde.

Stroustrups Idee war es so etwas wie zu kompilieren 

delete p; p = 0;
// ...
delete p;

delete selbst kann den Zeiger nicht auf Null setzen, da er void * übergeben hat, aber nicht void *&

Allerdings finde ich null heraus, dass p nicht sehr hilfreich ist, da möglicherweise andere Kopien dieses Zeigers vorhanden sind, die versehentlich gelöscht werden könnten. Ein besserer Weg ist die Verwendung der verschiedenen Arten von Smartpointern.

Spec 6.7

Sagt, dass jeder Zeiger mit der Adresse des Zeigers p nach einem Löschvorgang p ungültig ist (auf definierte Weise einer Implementierung). Es sagt nichts über das Ändern der Zeigeradresse aus, es ist weder erlaubt noch verboten.

Spec 6.9

Voraussetzung von 6.9 ist ein Objekt (gültig oder nicht). Diese Spezifikation gilt hier nicht, da p (die Adresse) nach dem Löschen nicht mehr gültig ist und daher NICHT auf ein Objekt zeigt. Es gibt also keinen Widerspruch und jede Diskussion, ob 6.7 oder 6.9 stärker ist, ist ungültig.

Die Spezifikation erfordert auch das Kopieren der Bytes an den ursprünglichen Objektspeicherort, was Ihr Code nicht tut und auch nicht möglich wäre, da das ursprüngliche Objekt gelöscht wurde.


Ich sehe jedoch keinen Grund, eine Zeigeradresse in ein Char-Array zu packen und es zu übergeben. Und Zeiger haben in einer bestimmten Implementierung immer die gleiche Größe. Ihr Code ist nur eine umständliche Version von:

SomeObject *o = ...;

void *orgO = o;
delete o;

someHashtable.remove(orgO);
// someHashtable.remove(o); o might be set to 0

Dieser Code sieht jedoch immer noch seltsam aus. Um ein Objekt aus der Hashtabelle zu erhalten, benötigen Sie den Zeiger auf dieses Objekt. Warum nicht direkt den Zeiger verwenden?

Eine Hashtabelle sollte dabei helfen, Objekte anhand einiger invarianter Werte der Objekte zu finden. Das ist nicht Ihre Anwendung der Hashtabelle

Wollten Sie eine Liste aller gültigen Instanzen von SomeObject haben?

Ihr Code ist ungültig Ursache Laut Stroustrup darf der Compiler p auf Null setzen. In diesem Fall stürzt Ihr Code ab 

2
stefan bachert

delete ist in [expr.delete] zum Aufrufen einer Deallocation-Funktion definiert, und Deallocation-Funktionen sind in [basic.stc.dynamic.deallocation] definiert als:

Jede Freigabefunktion muss void zurückgeben, und der erste Parameter muss void* sein.

Da alle Freigabefunktionen void* und nicht void*& erhalten, gibt es keinen Mechanismus, mit dem sie ihre Parameter ändern können. 

2
Barry

Die Umleitung durch einen ungültigen Zeigerwert und das Übergeben eines ungültigen Zeigerwerts an eine Freigabe-Funktion haben ein undefiniertes Verhalten. Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten.

Nach delete ptr; wird der Wert von ptr ein ungültiger Zeigerwert, und die Verwendung dieses Werts hat ein implementierungsdefiniertes Verhalten.

Der Standard besagt, dass der übergebene Zeigerwert "ungültig wird", dh sein Status wurde geändert, sodass bestimmte Aufrufe undefiniert werden und die Implementierung ihn anders behandeln kann.

Die Sprache ist nicht sehr klar, aber hier ist der Kontext:

6.7 Lagerdauer
4 Wenn das Ende der Dauer eines Speicherbereichs erreicht ist, werden die Werte aller Zeiger, die die Adresse eines beliebigen Teils dieses Speicherbereichs darstellen, zu ungültigen Zeigerwerten (6.9.2). Die Umleitung durch einen ungültigen Zeigerwert und das Übergeben eines ungültigen Zeigerwerts an eine Freigabe-Funktion haben ein undefiniertes Verhalten. Jede andere Verwendung eines ungültigen Zeigerwerts hat ein implementierungsdefiniertes Verhalten.

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

Es sind Werte des Typzeigers , die ungültig sind oder nicht, und sie "werden" also entsprechend dem Fortschritt der Ausführung eines Programms auf der abstrakten C++ - Maschine.

Der Standard spricht nicht über Änderungen, welchen Wert die von einem Wert adressierte Variable/Objekt hält oder die Zuordnung eines Symbols zu einem Wert.

C++ erlaubt explizit eine Implementierung von delete, um einen Lvalue-Operanden auf Null zu setzen, und ich hatte gehofft, dass Implementierungen dies tun würden, , Aber diese Idee scheint bei Implementierern nicht populär geworden zu sein.

Getrennt davon sagt Stroustrup, dass, wenn ein Operandenausdruck ein veränderbarer Wert war, dh ein Operandenausdruck die Adresse einer Variablen/eines Objekts war, die einen übergebenen Zeigerwert enthielt, wonach der Status dieses Werts " ungültig ", dann kann die Implementierung den von dieser Variablen/Objekt gehaltenen Wert auf Null setzen.

Es heißt jedoch nicht, dass sich der Wert von ptr ändern darf.

Stroustrup ist informell, wenn es darum geht, was eine Implementierung bewirken kann. Der Standard definiert, wie sich eine abstrakte C++ - Maschine verhalten kann/kann/darf. Hier spricht Stroustrup von einer hypothetischen Implementierung, die wie diese Maschine aussieht. Ein "ptr-Wert" kann sich "ändern", da das definierte und undefinierte Verhalten Sie nicht dazu veranlasst, herauszufinden, wie der Wert von Deallocation lautet, und das von der Implementierung definierte Verhalten kann alles sein, daher kann es sein, dass die Variable/Objekt hat einen anderen Wert.

Es macht keinen Sinn, über eine Wertänderung zu sprechen. Sie können einen Wert nicht "nullstellen"; Sie können eine Variable/ein Objekt auf Null setzen, und das ist es, was wir meinen, wenn wir sagen, dass ein Wert "null" ist - die Variable/das Objekt, auf das sie sich bezieht, wird auf Null gesetzt. Selbst wenn Sie "Zero out" strecken, um einen neuen Wert mit einem Namen oder Literal zu verknüpfen, kann die Implementierung dies tun, da Sie den Wert zur Laufzeit nicht über den Namen oder das Literal "verwenden" können, um herauszufinden, ob er noch mit ihm verbunden ist der gleiche Wert.

(Da ein Wert jedoch nur in einem Programm verwendet werden kann, muss er in einem Programm "verwendet" werden, indem er einen Wert, der eine Variable/ein Objekt, das ihn enthält, an einen Operator verweist, oder einen Verweis oder eine Konstante, die ihn angibt, an einen Operator weitergibt, und ein Operator dies kann so handeln, als ob ein anderer - Wert übergeben wurde, ich denke, Sie könnten einigermaßen informell sloppily das als "Werte ändernden Wert" in der Implementierung erfassen.)

Wenn der Inhalt dieses Arrays in das Objekt zurückkopiert wird, muss das Objekt anschließend seinen ursprünglichen Wert beibehalten.

Beim Kopieren wird es jedoch verwendet, sodass das Kopieren durch die Implementierung definiert wird, sobald es "ungültig" ist. Das Aufrufen eines Programms, das normalerweise kopiert wird, ist also durch die Implementierung definiert. Dies wird durch die Fußnote verdeutlicht, die das Beispiel dazu gibt 

Bei einigen Implementierungen kann es vorkommen, dass das Kopieren eines ungültigen Zeigerwerts einen vom System generierten Laufzeitfehler verursacht

Nichts tut das, was es normalerweise tut, undefiniertes/implementiertes Verhalten. Wir verwenden das normale Verhalten, um eine Abfolge von Änderungen an einer abstrakten Maschine zu bestimmen. Wenn eine durch die Implementierung definierte Zustandsänderung auftritt, verhalten sich die Dinge so, als würde die Implementierung sie als Verhalten definieren, nicht wie sie es normalerweise tun. Leider wird die Bedeutung von "Verwendung eines Wertes" nicht klargestellt. Ich weiß nicht, warum Sie meinen, 6.9 "garantiert" etwas mehr oder weniger als irgendwo anders, was nach einem undefinierten/implementierungsdefinierten Zustand nichts ist.

2
philipxy

Damit eine Löschfunktion den Zeiger aktualisieren kann, muss er die Adresse dieses Zeigers kennen und auch die Aktualisierung durchführen. Dies würde zusätzlichen Speicher, wenige zusätzliche Operationen und eine Compiler-Unterstützung erfordern. Sieht in Ihrem Beispiel mehr oder weniger trivial aus. 

Stellen Sie sich nun eine Kette von Funktionen vor, die den Zeiger in Argumenten aneinander übergeben und nur der letzte wirklich löschen wird. Welche Hinweise sind in einem solchen Fall zu aktualisieren? Der Letzte? Alles? Für letzteres müsste eine dynamische Liste von Zeigern erstellt werden:

Objec *o = ...
handle(o);
void handle(Object *o){
   if (deleteIt) doDelete(0);
   else doSomethingElseAndThenPossiblyDeleteIt(o);
}
void doDelete(Object *o) {
    delete o;
}

Wenn also der Löschvorgang seine Parameter ändern darf, würde er philosophisch eine Dose mit Warmen öffnen, wodurch die Effizienz des Programms verringert wird. Also ist es nicht erlaubt und ich hoffe, dass es niemals passieren wird. Das undefinierte Verhalten ist in diesen Fällen wahrscheinlich das Natürlichste. 

Beim Speicherinhalt habe ich leider zu viele Fehler gesehen, bei denen ein gelöschter Speicher überschrieben wird, nachdem der Zeiger gelöscht wurde. Und ... es funktioniert gut, bis ein Moment kommt. Da der Speicher als frei markiert ist, wird er von anderen Objekten wiederverwendet, was sehr uninteressante Folgen und viel Debugging zur Folge hat. Daher ist C++ auch in philosophischer Hinsicht keine einfache Sprache zum Programmieren. Es gibt andere Tools, mit denen sich diese Probleme ohne Sprachunterstützung beheben lassen. 

1
Serge