[C++] Gibt Placement New den Originalzeiger zurück?

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

[C++] Gibt Placement New den Originalzeiger zurück?

Beitrag von Krishty »

Microsoft hat diesen Bug geschlossen: https://developercommunity.visualstudio ... ation.html

Dort wird ein Array von Objekten auf einem vorbereiteten Speicherblock initialisiert:

  char data[100];
  Ptr* tab = reinterpret_cast<Ptr*>(data);
  new(tab) Ptr[3]{};


Dabei gibt das new unter bestimmten Umständen (siehe Link) eine andere Adresse zurück als übergeben wurde. Das ist nun der Streitpunkt – ob new das darf oder nicht.

Der Support-Chinese behauptet:
The standard only requires allocation function of non-allocating form to return the original pointer. See [new.delete.placement].

In your example, 'new (tab) Ptr[3]{}' is a placement new-expression which calls allocation function and does object initialization. In this case, the standard says (see [expr.new]):

When the allocation function returns a value other than null, it must be a pointer to a block of storage
in which space for the object has been reserved. The block of storage is assumed to be appropriately aligned
and of the requested size. The address of the created object will not necessarily be the same as that of the
block if the object is an array.

So even if the allocation function returns the original pointer, the address of the created object can be different from it.
Ich verstehe den Unterschied zwischen Allocating Form und non-Allocating Form leider nicht. „'new (tab) Ptr[3]{}' is a placement new-expression which calls allocation function“ klingt für mich völlig falsch; welches Placement new ruft denn eine Allocation Funktion auf?! Hat jemand einen C++-Standard zur Hand, um das zu klären?
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: [C++] Gibt Placement New den Originalzeiger zurück?

Beitrag von xq »

cppreference.com sagt:
cppreference.com hat geschrieben:If placement_params are provided, they are passed to the allocation function as additional arguments. Such allocation functions are known as "placement new", after the standard allocation function void* operator new(std::size_t, void*), which simply returns its second argument unchanged.
Das sagt wohl, dass das placement new mit Adresse diese Adresse zurückgeben muss, man kann aber auch diesen Operator überladen...
Ich würde mich nicht darauf verlassen, dass die selbe Adresse zurückkommt, auch weil so kram wie virtuelle Tabellen und so ja auch im Objekt konstruiert werden
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: [C++] Gibt Placement New den Originalzeiger zurück?

Beitrag von dot »

Krishty hat geschrieben:Dabei gibt das new unter bestimmten Umständen (siehe Link) eine andere Adresse zurück als übergeben wurde. Das ist nun der Streitpunkt – ob new das darf oder nicht.
tl;dr: new darf das imho definitiv. Abgesehen davon hat der Code in diesem Bugreport Undefined Behavior. Sogar dann noch, wenn wir das new komplett aus der Betrachtung nehmen.

Grund für die ganze Verwirrung sind wohl einige Missverständnisse was Terminologie betrifft. Fangen wir an mit dem Beispiel Operator Overloading, wo es gerne zu ähnlichen Verwechslungen kommt: Bei einer Funktion mit Namen operator + handelt es sich nicht um den + Operator. Der + Operator ist der + Operator. Der + Operator ist ein Syntaxelement einer additive-expression. Eine Funktion mit Namen operator + ist eine operator function. Der Compiler ruft als Teil der Evaluierung einer entsprechenden Expression mit entsprechenden Operanden in bestimmten Fällen eine operator function auf. Die operator function ist aber nicht der Operator. Der Operator verwendet die operator function.

Wesentlich für den konkreten Fall hier ist die Unterscheidung zwischen einer new-expression und einer allocation function bzw. deallocation function. So etwas wie new int ist eine new-expression. Eine new-expression erzeugt Objekte. Die Evaluierung einer new-expression kann (seit C++14 mit Betonung auf kann) eine allocation function aufrufen um Speicher für die zu erzeugenden Objekte zu besorgen. Eine allocation function bzw. deallocation function ist eine Funktion mit einer declarator-id der Form operator new oder operator new[] bzw. operator delete oder operator delete[]. Eine new-expression liefert einen Pointer auf das erzeugte Objekt bzw. einen Pointer auf das erste Element im Falle dass das erzeugte Objekt ein Array ist. Es gibt keine expliziten Garantien bezüglich eines Zusammenhangs zwischen dem Resultat einer new-expression und einem Pointer der aus dem Aufruf einer allocation function kam (abgesehen von einer Ausnahme die hier nicht Relevant ist).

Eine new-expression kann ein new-placement enthalten. Ein new-placement gibt eine Liste von Expressions die als zusätzliche Parameter an die allocation function übergeben werden. Eine new-expression die ein new-placement enthält wird als placement new-expression bezeichnet. new(tab) Ptr[3]{} ist eine placement new-expression. Nicht weil hier konzeptionell ein Objekt in einem vorhandenen Objekt "platziert" wird, sondern weil es sich um eine new-expression mit einem new-placement handelt. new (1, 2, 3) int; ist auch eine placement new-expression. (Die Bezeichnung kommt ursprünglich vermutlich aber wohl von der Benutzung im Zusammenhang mit dem "Platzieren" von Objekten.)

Der Standard definiert eine Reihe von allocation und deallocation functions als Teil der Standard Library. Dabei wird zwischen single-object, array und non-allocating forms unterschieden. void* operator new(std::size_t size, void* ptr) noexcept ist eine der standard allocation functions die zu den non-allocating forms gehören. Non-allocating eben weil diese allocation functions keinen neuen Speicher allokieren, sondern in der Tat per Definition einfach nur den übergebenen Pointer returnen. Unsere placement-new expression new(tab) Ptr[3]{} ruft genau ebendiese non-allocating allocation function (ja, den Term muss man sich auf der Zunge zergehen lassen) auf um sich den Speicher für das zu erzeugende Array zu besorgen. Und die allocation function returned einfach den übergebenen Pointer und die new-expression konstruiert das zu konstruierende Objekt dann im Speicher auf den dieser Pointer zeigt. Und das alles läuft am Ende dann darauf hinaus, dass unser Array tatsächlich in die gegebene Storage "platziert" wird.

An dieser Stelle wird klar: Man kann es einem nicht übel nehmen wenn man sich in dieser Terminologie, vermutlich ohne es zu merken, etwas verirrt. (Wie wir alle wissen, hat C++ hat ja einen merkwürdigen Hang dazu, dass alle Terminologie auf einmal komplett schiefläuft, wann immer es ans Allokieren geht…)

Aber nun zur eigentlichen Frage:
Krishty hat geschrieben: Der Support-Chinese behauptet:
The standard only requires allocation function of non-allocating form to return the original pointer. See [new.delete.placement].

In your example, 'new (tab) Ptr[3]{}' is a placement new-expression which calls allocation function and does object initialization. In this case, the standard says (see [expr.new]):

When the allocation function returns a value other than null, it must be a pointer to a block of storage
in which space for the object has been reserved. The block of storage is assumed to be appropriately aligned
and of the requested size. The address of the created object will not necessarily be the same as that of the
block if the object is an array.

So even if the allocation function returns the original pointer, the address of the created object can be different from it.
Ich verstehe den Unterschied zwischen Allocating Form und non-Allocating Form leider nicht. „'new (tab) Ptr[3]{}' is a placement new-expression which calls allocation function“ klingt für mich völlig falsch; welches Placement new ruft denn eine Allocation Funktion auf?! Hat jemand einen C++-Standard zur Hand, um das zu klären?
Der "Support-Chinese" ist ein Compiler Engineer und hat recht. ;) Während die non-allocating allocation functions in der Tat den übergebenen Pointer unverändert returnen, gibt es keine solche Garantie für die new-expression welche diese allocation function aufruft. Der Standard vermerkt diesen Umstand sogar explizit in einer Note, welche Herr Xiang Fan auch korrekt zitiert:
[expr.new] §18 hat geschrieben:[…] The address of the created object will not necessarily be the same as that of the block if the object is an array.
Schauen wir uns den fraglichen Code mal an:

Code: Alles auswählen

Ptr* tab = reinterpret_cast<Ptr*>(data);
new(tab) Ptr[3]{};

for (int i = 0; i < 3; i++)
std::cout << tab[i].handle << std::endl;
Eigentlich geht es hier um etwas anderes als die Frage was genau diese placement new-expression zurückliefert. Wie sollte es auch? Das Resultat der Expression wird doch völlig ignoriert!? Dieser Code geht davon aus dass new(tab) Ptr[3]{} ein Array von drei Ptr Objekten an Adresse tab konstruiert. Ja, man könnte einen Haufen Ausnahmen einführen, die z.B. im Fall von Array mit konstanter Größe, trivialem Elementtyp und Verwendung einer non-allocating allocation function eine solche Garantie abgibt. Aber wäre es das wirklich Wert? Der new Kram ist imo so schon kompliziert genug. Im Allgemeinen muss die new-expression mit einer dynamischen Anzahl an Elementen zurechtkommen. Im Allgemeinen muss die new-expression damit zurechtkommen, dass an irgendeinem Punkt in der Konstruktion des Array eine Exception fliegt und alle bisher konstruierten Elemente in umgekehrter Reihenfolge zerstört werden müssen. Im Allgemeinen muss eine passende delete-expression nur basierend auf dem von einer new-expression gelieferten Pointer das Array zerstören können. Irgendwo muss der Compiler da Information über die Anzahl der konstruierten Objekte in diesem Array unterbringen. Der einzige Speicher der ihm zur Verfügung steht ist der auf den tab zeigt. Der Compiler hat im Allgemeinen keine Möglichkeit, die Größe einer Allocation abzufragen, die Information ans Ende packen ist also auch nicht wirklich eine Option. Eine Garantie dass eine solche new-expression das erste Element an der gegebenen Adresse konstruiert, würde eine sinnvolle Implementierung im Allgemeinen also effektiv unmöglich machen (würde erfordern dass der Compiler zusätzlich interne Datenstrukturen unbeschränkter Größe anlegt um Allocations zu tracken). Genau darum gibt es keine solche Garantie. Der Compiler war hier allerdings zumindest smart genug, zu checken, dass er sich all das sparen kann so lange Ptr einen trivialen Destruktor hat. Darum gehen die Dinge in der Praxis hier erst schief sobald Ptr keinen trivialen Destruktor mehr hat…

Übrigens, selbst wenn ich den Code so umschreibe:

Code: Alles auswählen

Ptr data[3];
Ptr* tab = reinterpret_cast<Ptr*>(&data);

for (int i = 0; i < 3; i++)
std::cout << tab[i].handle << std::endl;
ist tab kein gültiger Zeiger in ein Array und ein Lesen von tab damit Undefined Behavior. Ein Array und das erste Element eines Arrays sind nicht pointer interconvertible. Mit anderen Worten: Es ist per Definition nicht möglich aus der Adresse eines Array einen gültigen Pointer auf das erste Element abzuleiten…


Den aktuellen Draft des C++ Standard schön als HTML aufbereitet gibt's übrigens immer hier: http://eel.is/c++draft/

MasterQ32 hat geschrieben:cppreference.com sagt:
cppreference.com hat geschrieben:If placement_params are provided, they are passed to the allocation function as additional arguments. Such allocation functions are known as "placement new", after the standard allocation function void* operator new(std::size_t, void*), which simply returns its second argument unchanged.


Das sagt wohl, dass das placement new mit Adresse diese Adresse zurückgeben muss, man kann aber auch diesen Operator überladen...

Nope, diese allocation function ist nicht replaceable
;)


PS: Was wirklich cool wäre, wäre Syntax Highlighting für inline Code…
PPS: Die beiden

Code: Alles auswählen

 Blöck da oben werfen irgendwie die Formatierung komplett aus der Bahn. Interessanterweise offenbar nur wenn sie in exakt dieser Form an bestimmten Stellen im Posting platziert werden…
Zuletzt geändert von dot am 18.11.2018, 14:54, insgesamt 1-mal geändert.
Benutzeravatar
xq
Establishment
Beiträge: 1581
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: [C++] Gibt Placement New den Originalzeiger zurück?

Beitrag von xq »

dot hat geschrieben:...
Danke für die schöne Erklärung. So geht ich heute schon mal nicht dumm ins Bett !
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Krishty
Establishment
Beiträge: 8229
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++] Gibt Placement New den Originalzeiger zurück?

Beitrag von Krishty »

An dieser Stelle wird klar: Man kann es einem nicht übel nehmen wenn man sich in dieser Terminologie, vermutlich ohne es zu merken, etwas verirrt.
Umso mehr Dank, dass du das so detailliert aufgearbeitet hast! Danke, danke!
Krishty hat geschrieben:Der "Support-Chinese" ist ein Compiler Engineer
Tatsache – ich hatte nicht genau auf den Namen geachtet und ihn mit dem eindeutigen Support-Chinesen darüber verwechselt, der es nicht auf die Kette gekriegt hat, eine nummerierte Liste abzuarbeiten.
Ein Array und das erste Element eines Arrays sind nicht pointer interconvertible. Mit anderen Worten: Es ist per Definition nicht möglich aus der Adresse eines Array einen gültigen Pointer auf das erste Element abzuleiten…
Zum Verständnis: Das bezieht sich aber nur auf reinterpret_cast, oder? Array to Pointer Decay gibt mir definitiv die Adresse des ersten Elements zurück?
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: [C++] Gibt Placement New den Originalzeiger zurück?

Beitrag von dot »

Krishty hat geschrieben:
Ein Array und das erste Element eines Arrays sind nicht pointer interconvertible. Mit anderen Worten: Es ist per Definition nicht möglich aus der Adresse eines Array einen gültigen Pointer auf das erste Element abzuleiten…
Zum Verständnis: Das bezieht sich aber nur auf reinterpret_cast, oder? Array to Pointer Decay gibt mir definitiv die Adresse des ersten Elements zurück?
Genau. Es gibt eine implizite Konvertierung von Array nach Pointer auf erstes Element, gemeinhin als "Array to Pointer Decay" bekannt. Ein Array und sein erstes Element haben effektiv sogar garantiert die selbe Adresse. Ein Pointer auf ein Array kann aber dennoch nicht in einen Pointer auf das erste Element gecastet werden:

Code: Alles auswählen

int arr[3];
auto parr = &arr;

int* blub = arr;  // OK
int* blab = static_cast<int*>(*parr);  // OK, but unnecessary
int* blob = reinterpret_cast<int*>(*parr);  // OK, but weird
int* bleb = *parr;  // OK

int* blib = reinterpret_cast<int*>(parr);  // not great, but still OK
blib[0];  // still OK
blib[0] = 42;  // not OK
Antworten