static_cast von void* auf Derived und FirstBase



  • Arcoth schrieb:

    hustbaer schrieb:

    Arcoth schrieb:

    &m_storage.bytes[0] ist ein Zeiger auf das erste Element eines char Arrays. Dieser Zeiger ist nicht mit dem Objekt, für welches dieses Array Speicher bereitstellt, pointer-interconvertible.

    OK. Allerdings verstehe ich nicht warum das ein Thema ist. Ich verwende das char Array nie als char Array, also gibt es nichts was die "pointer optimization barrier" Blocken müsste.

    Du verstehst das vielleicht falsch. Die Optimierung besteht doch darin, dass wir schauen, ob irgendwelche Verweise auf das Objekt bestehen. Du hast deinen Zeiger quasi aus dem Finger gezogen; das kann der Optimizer nicht nachvollziehen.

    Kann leicht sein dass ich das falsch verstehe. Denn jetzt, nach dieser Antwort, denke ich mir: Wenn der Optimizer das nicht nachvollziehen kann, dann kann er wohl auch nix optimieren, und dann sollte keine "pointer optimization barrier" notwendig sein.

    Arcoth schrieb:

    hustbaer schrieb:

    Arcoth schrieb:

    Ergo müssen wir launder n. Dabei ist zu beachten, dass das char Array genau sizeof T groß sein muss, sonst gibt es Bytes die durch den Zeiger auf das erste Element reachable sind, aber nicht durch den Zeiger auf T .

    Ist nicht der Fall, das char Array kann grösser sein - ist ja ein Variant, und die Typen die darin "leben" können sind nicht alle gleich gross.

    Das ist kein Problem: du musst dann halt deinen Zeiger anders beziehen. Beispielsweise als den Rückgabewert von placement new.

    Keine Ahnung wie du das aus dem zitierten Absatz rausliest. Da steht nichts über Arrays drinnen. Und was da über Bytes steht, kann ich nur so verstehen, dass über das Resultat nicht mehr Speicher erreichbar sein darf als über den ursprünglichen Zeiger.
    Wobei ich nicht weiss wie das gemeint ist, da beide den selben Typ haben. Und wie soll dann über den einen Zeiger mehr oder weniger Speicher erreichbar sein als über den anderen?

    Und... wohl ist das ein Problem! Weil Typen wie boost::variant vermutlich keine Lust haben das Ergebnis von placement new zu speichern. Völlig unnötiger Overhead, die Adresse und welches Objekt darin jetzt lebt (welcher Typ) sind ja bekannt.

    Arcoth schrieb:

    Aber das nur so nebenbei, ich will ja den T* Zeiger nirgends aufheben, ich weiss ja die Adresse des Objekts sowieso - weil ich das Storage Array kenne wo ich es reinkonstruiert habe.

    Der Punkt ist aber, dass du den Zeiger legitim herleiten musst, damit der Optimizer weiß, auf welches Objekt du zugreifst.

    Verstehe ich nicht. Entweder der Optimizer weiss es, dann kann er optimieren. Oder er weiss es nicht, dann kann er halt nicht optimieren.

    Vielleicht mal anders rum gefragt, vielleicht verstehe ich es dann: Welche Optimierungen sollen dadurch möglich werden, die es ohne diese Regel(n) nicht sind?

    Arcoth schrieb:

    Aber davon abgesehen dass ich das nicht ganz verstehe: Gibt es einen Weg zu vermeiden dass man bei sowas (Variants) std::launder bzw. Probleme mit fehlendem std::launder vermeiden kann?

    Du hörst dich gerade an wie ein Anfänger an, der versucht, fundamentale Schwierigkeiten in einer Sprache mit Faustregeln wegzuerklären. Nein, du musst einfach verstehen, wie die Zeigersemantik funktioniert. Das musste man eigentlich aber schon immer; pointer provenance war stets ein Thema. Wir haben schon vor Jahren mit Krümelkacker darüber diskutiert, ob es erlaubt ist, durch ein multidimensionales Array flach zu iterieren. Mit den aktuellen Regeln ist es unmöglich. Meine Antwort hier hat von etwas ähnlichem gehandelt. Ich kann mir aber nur schwer vorstellen, dass jemand, der so erfahren ist wie du, auch nur annähernd Schwierigkeiten hat, das zu lernen.

    Wenn die Regeln irgendwo in lesbarem Englisch und idealerweise mit ausreichend Beispielen + Erklärung zu den Beispielen dokumentiert wären, dann wahrscheinlich ja, dann könnte ich das wohl in relativ kurzer Zeit lernen. Was man aber vermutlich schnell vergisst wenn man anfängt sich in den Standard einzuarbeiten: das Ding (der Standard) ist für nicht-Experten quasi unlesbar. Ich bin Software-Entwickler und kein Standardwälzer.


  • Mod

    hustbaer schrieb:

    Wenn der Optimizer das nicht nachvollziehen kann, dann kann er wohl auch nix optimieren

    Er wird trotzdem optimieren. Es wird nur nicht in Einklang mit dem sein, was du erwartest. Weil du dich nicht an den Standard hältst.

    hustbaer schrieb:

    Keine Ahnung wie du das aus dem zitierten Absatz rausliest. Da steht nichts über Arrays drinnen. Und was da über Bytes steht, kann ich nur so verstehen, dass über das Resultat nicht mehr Speicher erreichbar sein darf als über den ursprünglichen Zeiger.

    Doch, da steht was über Arrays drinnen. Lies doch mal.

    <a href= schrieb:

    [ptr.launder]/3">A byte of storage is reachable through a pointer value that points to an object Y if it is within the storage occupied by Y , an object that is pointer-interconvertible with Y , or the immediately-enclosing array object if Y is an array element.

    Ein Zeiger auf ein Element von bytes ist demnach durch jeden anderen Zeiger auf ein Element von bytes reachable. Aber: Ein Zeiger auf ein Objekt in bytes kann nicht auf Bytes reachen, die außerhalb des von ihm belegten Speichers liegen. Ergo wäre so ein launder nicht möglich.

    Wobei ich nicht weiss wie das gemeint ist, da beide den selben Typ haben.

    Welche beiden?

    Und... wohl ist das ein Problem! Weil Typen wie boost::variant vermutlich keine Lust haben das Ergebnis von placement new zu speichern. Völlig unnötiger Overhead, die Adresse und welches Objekt darin jetzt lebt (welcher Typ) sind ja bekannt.

    Die Lösung dafür braucht auch keinen zusätzlichen Zeiger. Statt die Objekte selbst im char Array zu speichern, welche von unterschiedlicher Größe sein könnten, speichern wir einen Wrapper, den wir manuell so padden, dass er die Größe des größten aller Typen hat. Geht relativ elegant mit einer union , die ein entsprechendes char Array hat. Dann noch einen static_assert draufhauen, dass die union s alle auch wirklich gleich groß sind. Diese union s konstruieren wir dann im char Array; da sie groß genug sind, sind alle Bytes reachable. :p 👍

    Arcoth schrieb:

    Aber das nur so nebenbei, ich will ja den T* Zeiger nirgends aufheben, ich weiss ja die Adresse des Objekts sowieso - weil ich das Storage Array kenne wo ich es reinkonstruiert habe.

    Der Punkt ist aber, dass du den Zeiger legitim herleiten musst, damit der Optimizer weiß, auf welches Objekt du zugreifst.

    Verstehe ich nicht. Entweder der Optimizer weiss es, dann kann er optimieren. Oder er weiss es nicht, dann kann er halt nicht optimieren.

    Das ist doch der springende Punkt: Der Optimizer optimiert auch, wenn du Dinge tust, von denen er nicht weiß, wenn du sie auf eine Art und Weise abwickelst, die nicht den Regeln entsprechen! Deswegen gibt es doch solche Einschränkungen: damit der Optimizer Annahmen machen darf, die es erlauben, schnelleren Code zu produzieren. Aber wem sag ich das?!

    Vielleicht mal anders rum gefragt, vielleicht verstehe ich es dann: Welche Optimierungen sollen dadurch möglich werden, die es ohne diese Regel(n) nicht sind?

    Common subexpression elimination, dead store elimination, Aliasing Optimierungen, etc. In vielen Fällen ist es von entscheidender Bedeutung, ob ein gewisser Zeiger nun auf ein Objekt zeigt oder nicht. Ich kann auch gerne Beispiele zeigen (aber ohne Garantie, dass GCC da tatsächlich was reißt 😉 ).

    Wenn die Regeln irgendwo in lesbarem Englisch und idealerweise mit ausreichend Beispielen + Erklärung zu den Beispielen dokumentiert wären, dann wahrscheinlich ja, dann könnte ich das wohl in relativ kurzer Zeit lernen.

    Dann musst du nur kurz warten; ich bin sicher, jemand schreibt bald einen Artikel dazu. Oder vielleicht gibt es den schon.



  • Arcoth schrieb:

    hustbaer schrieb:

    Wenn der Optimizer das nicht nachvollziehen kann, dann kann er wohl auch nix optimieren

    Er wird trotzdem optimieren. Es wird nur nicht in Einklang mit dem sein, was du erwartest. Weil du dich nicht an den Standard hältst.

    Jain.
    Die Beispiele wo ein "alter" Zeiger wiederverwendet wird, verstehe ich.
    Wenn du aber schreibst ich müsse statt

    T* p = ...
    ...
    p = reinterpret_cast<T*>(&m_storage[0]);
    

    richtigerweise

    T* p = ...
    ...
    p = std::launder(reinterpret_cast<T*>(&m_storage[0]));
    // oder
    p = reinterpret_cast<T*>(std::launder(&m_storage[0])); // ???
    

    schreiben, dann verstehe ich es nicht.
    Denn der Compiler sieht hier die Zuweisung p = ..., und kann daher verstehen dass er nicht versteht welchen Wert p jetzt bekommt. Er kann doch nicht einfach annehmen dass alles was er nicht versteht bedeutet dass p noch auf das alte Objekt zeigt. Das wäre total beknackt!

    D.h. die Frage ist vielmehr: Darf der Compiler annehmen dass so ein reinterpret_cast<T*>(&m_storage[0]) Konstrukt, sofern die selbe Adresse dabei rauskommt, auch immer auf das selbe Objekt zeigt? (Also speziell nicht auf ein neues Objekts des selben Typs an der selben Adresse.)

    Arcoth schrieb:

    hustbaer schrieb:

    Keine Ahnung wie du das aus dem zitierten Absatz rausliest. Da steht nichts über Arrays drinnen. Und was da über Bytes steht, kann ich nur so verstehen, dass über das Resultat nicht mehr Speicher erreichbar sein darf als über den ursprünglichen Zeiger.

    Doch, da steht was über Arrays drinnen. Lies doch mal.

    OK, hab' ich übersehen.

    Arcoth schrieb:

    Wobei ich nicht weiss wie das gemeint ist, da beide den selben Typ haben.

    Welche beiden?

    Na Parameter und Returnwert von std::launder.

    Arcoth schrieb:

    Und... wohl ist das ein Problem! Weil Typen wie boost::variant vermutlich keine Lust haben das Ergebnis von placement new zu speichern. Völlig unnötiger Overhead, die Adresse und welches Objekt darin jetzt lebt (welcher Typ) sind ja bekannt.

    Die Lösung dafür braucht auch keinen zusätzlichen Zeiger. Statt die Objekte selbst im char Array zu speichern, welche von unterschiedlicher Größe sein könnten, speichern wir einen Wrapper, den wir manuell so padden, dass er die Größe des größten aller Typen hat. Geht relativ elegant mit einer union , die ein entsprechendes char Array hat. Dann noch einen static_assert draufhauen, dass die union s alle auch wirklich gleich groß sind. Diese union s konstruieren wir dann im char Array; da sie groß genug sind, sind alle Bytes reachable. :p 👍

    Ein Ruleset dass das von mir verlangt, nur um irgendwelchen komischen Regeln gerecht zu werden, würde ich als kompletten Mist bezeichnen.

    Aber nochmal: ich kann das aus dem Text da nicht rauslesen. Da steht bloss dass über das Resultat von std::launder nicht mehr erreichbar sein darf als über den Parameter. Der umgekehrte Fall, also dass über den Parameter mehr erreichbar ist, scheint mir durchaus erlaubt zu sein. Desweiteren sind aber Parameter und Returnwert beide vom Typ T*, d.h. über beide ist sowieso exakt die selbe Menge an Bytes erreichbar. Ich vermute fast dass hier einfach der Text schlecht formuliert ist.

    Der Optimizer optimiert auch, wenn du Dinge tust, von denen er nicht weiß, wenn du sie auf eine Art und Weise abwickelst, die nicht den Regeln entsprechen! Deswegen gibt es doch solche Einschränkungen: damit der Optimizer Annahmen machen darf, die es erlauben, schnelleren Code zu produzieren. Aber wem sag ich das?!

    Siehe oben: für den Fall ganz ohne Zuweisung, wo quasi der alte Zeiger weiterverwendet wird, kann ich das verstehen. Für den Fall wo die Adresse jedes mal neu "aus der Luft gezogen wird", nicht ganz. Dann kann er solche Konstrukte halt nicht optimieren. Oder besser: nur wie bisher, d.h. wenn er "sehen" kann dass es so oder so keinen Unterschied machen kann.

    Common subexpression elimination, dead store elimination, Aliasing Optimierungen, etc. In vielen Fällen ist es von entscheidender Bedeutung, ob ein gewisser Zeiger nun auf ein Objekt zeigt oder nicht. Ich kann auch gerne Beispiele zeigen (aber ohne Garantie, dass GCC da tatsächlich was reißt 😉 ).

    Was genau ist dead store elimination? Und was für Aliasing Optimierungen meinst du?

    Desweiteren... das sind doch alles Optimierungen die der Compiler sowieso schon machen kann. "As if" halt. Sicher, wenn jetzt jemand die Adresse eines Objekts an eine "undurchsichtige" Funktion übergibt, dann muss der Compiler damit rechnen dass das Objekt von der Funktion modifiziert wurde - und danach Werte neu aus dem Speicher laden, auch wenn er das davor schon getan hat. Muss er aber dann auch noch, nur halt mit Ausnahme der const und Reference-Member. Und da const und Reference-Member eigentlich eher selten sind...

    Ich bin mir nicht sicher ob die Optimierungen die damit legal werden dafür stehen dass wieder zigtausende von C++ Programmierern mehr falschen Code schreiben, weil sie die Regeln nicht verstanden haben (bzw. vermutlich öfter: von den Regeln noch nichtmal was gehört haben).

    Arcoth schrieb:

    Wenn die Regeln irgendwo in lesbarem Englisch und idealerweise mit ausreichend Beispielen + Erklärung zu den Beispielen dokumentiert wären, dann wahrscheinlich ja, dann könnte ich das wohl in relativ kurzer Zeit lernen.

    Dann musst du nur kurz warten; ich bin sicher, jemand schreibt bald einen Artikel dazu. Oder vielleicht gibt es den schon.

    Also ich hab vor ein paar Monaten das letzte mal ausgiebiger gegoogelt, und da hab' ich diesbezüglich nichts gefunden.



  • p.s.:
    Wenn ich das richtig verstehe würde das auch die meisten Container betreffen. D.h. z.B. dieser Code wäre dann falsch:

    vector<std::string> v;
    v.reserve(2);
    v.push_back("foo");
    std::string* p = &v[0];
    v.push_back("bar");
    std::cout << p[1];
    v.pop_back();
    v.push_back("baz");
    std::cout << p[1];
    

    ?



  • Der war schon immer falsch, da Iteratoren beim Hinzufügen und Entfernen nicht mehr gültig sind.
    Ach, da ist ja noch ein reserve 🙄



  • v.reserve(2);

    Und wo siehst du da Iteratoren?


  • Mod

    Techel schrieb:

    Der war schon immer falsch, da Iteratoren beim Hinzufügen und Entfernen nicht mehr gültig sind.

    Bloedsinn.

    hustbaer schrieb:

    p.s.:
    Wenn ich das richtig verstehe würde das auch die meisten Container betreffen. D.h. z.B. dieser Code wäre dann falsch:

    vector<std::string> v;
    v.reserve(2);
    v.push_back("foo");
    std::string* p = &v[0];
    v.push_back("bar");
    std::cout << p[1];
    v.pop_back();
    v.push_back("baz");
    std::cout << p[1];
    

    ?

    Wenn wir von Containern sprechen, gibt es ein ganz anderes Problem bzgl. P0137. Container allozieren rohen Speicher und packen dort ihre Objekte mittels placement new rein. Das bedeutet aber, dass Zeigerarithmetik nicht funktioniert, da bspw. p+1 ein pointer past-the-end (des ersten Objekts) ist. In anderen Worten, Zeigerarithmetik funktioniert nur ueber Arrays, und das gibt es hier nicht.

    Auf den anderen Beitrag werde ich heute Nacht antworten muessen.


  • Mod

    hustbaer schrieb:

    Denn der Compiler sieht hier die Zuweisung p = ..., und kann daher verstehen dass er nicht versteht welchen Wert p jetzt bekommt. Er kann doch nicht einfach annehmen dass alles was er nicht versteht bedeutet dass p noch auf das alte Objekt zeigt. Das wäre total beknackt!

    Ich verstehe nicht, auf welches Beispiel du dich beziehst. Zeig mal eins.

    D.h. die Frage ist vielmehr: Darf der Compiler annehmen dass so ein reinterpret_cast<T*>(&m_storage[0]) Konstrukt, sofern die selbe Adresse dabei rauskommt, auch immer auf das selbe Objekt zeigt?

    Der Compiler weiß, dass reinterpret_cast<T*>(&m_storage[0]) auf das erste Element von m_storage zeigt. Mehr nicht.

    Aber nochmal: ich kann das aus dem Text da nicht rauslesen. Da steht bloss dass über das Resultat von std::launder nicht mehr erreichbar sein darf als über den Parameter. Der umgekehrte Fall, also dass über den Parameter mehr erreichbar ist, scheint mir durchaus erlaubt zu sein.

    Oh, stimmt sogar, da habe ich mich verlesen. Ich hab mich schon gefragt, was diese Regel sollte, das macht nämlich andersherum keinen Sinn.

    Desweiteren sind aber Parameter und Returnwert beide vom Typ T*, d.h. über beide ist sowieso exakt die selbe Menge an Bytes erreichbar. Ich vermute fast dass hier einfach der Text schlecht formuliert ist.

    Um die Typen geht es gar nicht, sondern um die Pointees der Zeiger. Der Pointee hat mit dem Typen nichts mehr zu tun.


  • Mod

    hustbaer schrieb:

    Arcoth schrieb:

    hustbaer schrieb:

    Wenn der Optimizer das nicht nachvollziehen kann, dann kann er wohl auch nix optimieren

    Er wird trotzdem optimieren. Es wird nur nicht in Einklang mit dem sein, was du erwartest. Weil du dich nicht an den Standard hältst.

    Jain.
    Die Beispiele wo ein "alter" Zeiger wiederverwendet wird, verstehe ich.
    Wenn du aber schreibst ich müsse statt

    T* p = ...
    ...
    p = reinterpret_cast<T*>(&m_storage[0]);
    

    richtigerweise

    T* p = ...
    ...
    p = std::launder(reinterpret_cast<T*>(&m_storage[0]));
    // oder
    p = reinterpret_cast<T*>(std::launder(&m_storage[0])); // ???
    

    schreiben, dann verstehe ich es nicht.
    Denn der Compiler sieht hier die Zuweisung p = ..., und kann daher verstehen dass er nicht versteht welchen Wert p jetzt bekommt. Er kann doch nicht einfach annehmen dass alles was er nicht versteht bedeutet dass p noch auf das alte Objekt zeigt. Das wäre total beknackt!

    Du denkst hier viel zu pragmatisch. Die Regel besagt, dass ein Zeiger auf ein Objekt zeigt, bist du ihn launderst, oder bis ein neues Objekt gleichen Typs darin konstruiert wird, etc. hier hast du einfach einen pointer to char zu einem pointer to T gecastest; das aendert den Pointee nicht. So sind die Regeln. Die Regeln sind vielleicht nicht perfekt, aber wir muessen sowieso einen Kompromiss zwischen Simplizitaet und optimaler Semantik treffen; sobald wir anfangen, Sonderfaelle fuer "offensichtliche" Zuweisungen einzufuehren, wird dieses ganze Thema noch viel schwerer zu lehren als jetzt schon.

    Was die Optimierungen angeht: dead store elimination handelt davon, stores zu eliminieren, die die Semantik einer wohldefinierten Ausfuehrung des Programs nicht aendern wuerden. Bspw.

    void f(char*);
    
    int main() {
      char arr[][2] {1, 2, 3, 4, 5, 6};
      f(arr);
    }
    

    arr hat keine linkage, also kann f nicht direkt auf andere Unterarrays von arr zugreifen; das folgt aus der Definition von launder und pointer-interconvertibility. Also duerfen wir, falls arr nirgends sonst verwendet (oder ueberschrieben) wird, die stores auf arr[1] und arr[2] eliminieren.

    Das obige Beispiel mag ein wenig gekuenstelt erscheinen, aber in echten Funktionen gibt es branches etc. in denen solche Szenarien durchaus denkbar sind.

    Ein anderes Beispiel, dass sich auf die erwaehnte Optimierung von XL stuetzt:

    struct Foo {
        int a;
        int b;
        virtual void f();
    };
    
    Foo foo{1, 2};
    if (someBoolean)
        g(&foo.a);
    

    Der Optimizer sieht, dass g nicht auf andere Member von Foo zugreifen kann (weder mittels launder noch durch einen cast). Das heisst, dass die Initialisierung von foo.b hinter die Branch verschoben werden kann, und das ermoeglicht uns, diesen Store als branch delay slot zu verwenden. Das mag zwar nur in Prozessoren mit MIPS pipelines relevant sein, aber die allgemeine Idee von reordering ist durchaus relevant, e.g. wenn wir in der Lage sind, Zugriffe auf dieselbe cache line naeher zu bringen um Lokalitaet zu erhoehen.

    Also ich hab vor ein paar Monaten das letzte mal ausgiebiger gegoogelt, und da hab' ich diesbezüglich nichts gefunden.

    Ich kann den Autor im IRC nach ein paar grundlegenden Ideen fragen, und dann koennte ich das wohl zusammenfassen.



  • Arcoth schrieb:

    Du denkst hier viel zu pragmatisch.

    Das kann sein 😉

    Arcoth schrieb:

    Die Regel besagt, dass ein Zeiger auf ein Objekt zeigt, bist du ihn launderst, oder bis ein neues Objekt gleichen Typs darin konstruiert wird, etc. hier hast du einfach einen pointer to char zu einem pointer to T gecastest; das aendert den Pointee nicht. So sind die Regeln. Die Regeln sind vielleicht nicht perfekt, aber wir muessen sowieso einen Kompromiss zwischen Simplizitaet und optimaler Semantik treffen; sobald wir anfangen, Sonderfaelle fuer "offensichtliche" Zuweisungen einzufuehren, wird dieses ganze Thema noch viel schwerer zu lehren als jetzt schon.

    Ja, Sonderregeln sind schlecht. Wenn dann müsste die Sache mit anders formulierten, generischen Regeln erschlagen werden. Keine Ahnung ob das möglich ist. Bzw. für diesen Fall überhaupt sinnvoll.

    Man könnte die Idee aber auch verwerfen 😃 und statt dessen versuchen was anderes nettes zu machen - wie z.B. ne C++-taugliche Formulierung von "restrict".

    Arcoth schrieb:

    Was die Optimierungen angeht: dead store elimination handelt davon, stores zu eliminieren, die die Semantik einer wohldefinierten Ausfuehrung des Programs nicht aendern wuerden. Bspw.

    void f(char*);
    
    int main() {
      char arr[][2] {1, 2, 3, 4, 5, 6};
      f(arr);
    }
    

    arr hat keine linkage, also kann f nicht direkt auf andere Unterarrays von arr zugreifen; das folgt aus der Definition von launder und pointer-interconvertibility. Also duerfen wir, falls arr nirgends sonst verwendet (oder ueberschrieben) wird, die stores auf arr[1] und arr[2] eliminieren.

    OK, verstehe ich soweit. Gerade dieses Beispiel halte ich aber für recht problematisch. Ich würde schätzen dass das enorm viel Code bricht. Arrays-of-arrays sind in bestimmten Bereichen oft zu finden, und genau so oft findet man da vermutlich Code der dann mit Funktionen draufkloppt die nur "einfache" Arrays unterstützen. Ich denke da z.B. an Matrizen. Wobei mir dann gleich noch SIMD Libraries einfallen. Die sind zwar sowieso ein Problem wegen strict aliasing, aber selbst wenn das gelöst wäre (ala __attribute__((__may_alias__)) ) bliebe auch da das selbe Problem. Also dass man dann nur "einfache" Arrays verwenden könnte, wenn man vor hat mit einer SIMD Library draufzuklopfen.

    Arcoth schrieb:

    Das obige Beispiel mag ein wenig gekuenstelt erscheinen, aber in echten Funktionen gibt es branches etc. in denen solche Szenarien durchaus denkbar sind.

    Ja, schon klar. Allerdings hat man in echten Programmen auch oft LTCG, und wenn "f" in einem "LTCG-kompatiblen" .a/.o definiert ist, dann hätte der Compiler alles was er braucht um die Optimierung trotzdem zu machen -- selbst wenn er sich entscheidet "f" an der Stelle nicht zu inlinen 😉

    Arcoth schrieb:

    Ein anderes Beispiel, dass sich auf die erwaehnte Optimierung von XL stuetzt:

    struct Foo {
        int a;
        int b;
        virtual void f();
    };
    
    Foo foo{1, 2};
    if (someBoolean)
        g(&foo.a);
    

    Der Optimizer sieht, dass g nicht auf andere Member von Foo zugreifen kann (weder mittels launder noch durch einen cast). Das heisst, dass die Initialisierung von foo.b hinter die Branch verschoben werden kann, und das ermoeglicht uns, diesen Store als branch delay slot zu verwenden. Das mag zwar nur in Prozessoren mit MIPS pipelines relevant sein, aber die allgemeine Idee von reordering ist durchaus relevant, e.g. wenn wir in der Lage sind, Zugriffe auf dieselbe cache line naeher zu bringen um Lokalitaet zu erhoehen.

    Bzw. u.U. könnte er die Initialisierung von foo.b gleich ganz weglassen und nichtmal Storage dafür reservieren -- wenn da sonst nix mehr ist als der Code in deinem Beispiel sogar sicherlich.
    Auch das kann er aber sowieso, sobald er sich an der Stelle wo er den "g" Aufruf compilieren muss die Definition von "g" angucken kann.

    Und auch das halte ich für halbwegs problematisch.

    Wobei mir jetzt ein paar weitere Fragen zu dem Thema einfallen:
    * Was ist mit memcpy ? Soweit ich weiss darf man mit memcpy aktuell auch arrays-von-arrays kopieren. Wie sähe das dann mit der aktuellen Formulierung dieser "pointer-interconvertibility" Regeln aus?
    * Was ist mit handgestricktem memcpy ? Also nem Loop der ( unsigned ) char Pointer verwendet um Speicherblöcke zu kopieren.
    * Und wenn man die Sache für ( unsigned ) char erlaubt (so wie bei der Aliasing-Ausnahme für ( unsigned ) char ), dann erzeugt das gleich die nächste Falle in die man reintappen kann: mit ( unsigned ) char ) wäre die Funktion OK, mit int nicht mehr -- selbst wenn ein array-of-arrays-of- int übergeben wird.
    * Was ist mit Sachen wie

    struct RefCountedWString
    {
    	size_t length;
    	size_t refs;
    	wchar_t data[1];
    };
    
    wchar_t* Rcws_Create(wchar_t* sz)
    {
    	size_t const length = wcslen(sz);
    	RefCountedWString* const rcws = static_cast<RefCountedWString*>(malloc(sizeof(RefCountedWString) + sizeof(wchar_t) * length));
    	if (!rcws)
    		return 0;
    	rcws->length = length;
    	rcws->refs = 1;
    	memcpy(&rcws->data[0], sz, sizeof(wchar_t) * (length + 1));
    	return &rcws->data[0];
    }
    
    void Rcws_AddRef(wchar_t* s)
    {
    	if (s)
    	{
    		RefCountedWString* const rcws = reinterpret_cast<RefCountedWString*>(reinterpret_cast<char*>(s) - (sizeof(size_t) * 2));
    		rcws->refs++;
    	}
    }
    
    void Rcws_Release(wchar_t* s)
    {
    	if (s)
    	{
    		RefCountedWString* const rcws = reinterpret_cast<RefCountedWString*>(reinterpret_cast<char*>(s) - (sizeof(size_t) * 2));
    		rcws->refs--;
    		if (!rcws->refs)
    			free(rcws);
    	}
    }
    

    Soweit ich weiss ist das nach aktuellem Standard legal. Wäre dann ja wohl auch verboten, oder? Und dass es viel solchen und ähnlichen Code gibt, da bin ich mir halbwegs sicher.


  • Mod

    Arrays-of-arrays sind in bestimmten Bereichen oft zu finden, und genau so oft findet man da vermutlich Code der dann mit Funktionen draufkloppt die nur "einfache" Arrays unterstützen.

    Das ist an sich kein Problem, da multidimensionale Arrays sowieso nur Syntaxzucker sind; wir können sie einfach durch ein Template simulieren.

    hustbaer schrieb:

    Wobei mir jetzt ein paar weitere Fragen zu dem Thema einfallen:
    * Was ist mit memcpy ? Soweit ich weiss darf man mit memcpy aktuell auch arrays-von-arrays kopieren. Wie sähe das dann mit der aktuellen Formulierung dieser "pointer-interconvertibility" Regeln aus?

    Warum sollte das jetzt anders sein als vorher? memcpy wurde schon jeher über [basic.types]/3 unterstützt. Und im Übrigen wird die Implementierung von memcpy nicht vorgegeben, also muss man sich über die Semantik von Zeigerarithmetik keine Gedanken machen.

    * Was ist mit handgestricktem memcpy ? Also nem Loop der ( unsigned ) char Pointer verwendet um Speicherblöcke zu kopieren.

    Das ist laut aktuellem wording nicht möglich, da wir nicht über ein Array iterieren. Wobei das wohl in Ordnung ist, es gibt wahrscheinlich zu viel Code, der so etwas tut.

    * Und wenn man die Sache für ( unsigned ) char erlaubt (so wie bei der Aliasing-Ausnahme für ( unsigned ) char ), dann erzeugt das gleich die nächste Falle in die man reintappen kann: mit ( unsigned ) char ) wäre die Funktion OK, mit int nicht mehr -- selbst wenn ein array-of-arrays-of- int übergeben wird.

    Stimmt.

    * Was ist mit Sachen wie

    // [..]
    RefCountedWString* const rcws = static_cast<RefCountedWString*>(malloc(sizeof(RefCountedWString) + sizeof(wchar_t) * length));
    if (!rcws)
      return 0;
    rcws->length = length;
    

    Das war schon immer illegal. malloc erzeugt nämlich keine Objekte. Wird auch als drafting note in P0137 erwähnt:

    Drafting note: this maintains the status quo that malloc alone is not sufficient to create an object.

    Aber selbst wenn wir das mal übersehen, und davon ausgehen dass jemand dort ein placement new reinhaut, sticht immer noch

    RefCountedWString* const rcws = reinterpret_cast<RefCountedWString*>(reinterpret_cast<char*>(s) - (sizeof(size_t) * 2));
    

    ins Auge. Das wird definitiv nicht mehr wohldefiniert sein.


  • Mod

    Okay, ich habe mich gerade noch einmal mit dem Autor unterhalten.

    IRC/Richard Smith schrieb:

    suppose we have:

    struct X { virtual void f(); }; 
    struct Y : X { void f(); }; 
    struct Z : X { void f(); };
    alignas(Y, Z) char buf[max(sizeof(Y), sizeof(Z))];
    X *p = new (buf) Y;
    

    and now we do this:

    p->f(); p->f();
    

    [22:30:54] <zygoloid> maybe we're calling the virtual function in a loop or similar
    [22:30:58] <Arcoth> You're saying f does something
    [22:31:00] <Arcoth> funny
    [22:31:03] <zygoloid> well
    [22:31:04] <Arcoth> like replacing the objects
    [22:31:15] <zygoloid> it turns out to be important for performance that we don't load the vptr on each iteration
    [22:31:30] <zygoloid> and maybe we can even prove that p points to a Y object and devirtualize
    [22:31:38] <zygoloid> but yeah, what happens if f does something funny
    [22:32:13] <Arcoth> Good point.
    [22:32:23] <zygoloid> the language says that can't happen: if the pointer value in p points to a Y object, that same value is never going to point to a Z
    [22:32:48] <zygoloid> we have rules that say that the pointer transparently updates to point to new Y objects you create in the same storage
    [22:32:54] <zygoloid> but it does not transparently update to point to a Z object
    [22:33:01] <zygoloid> so the devirtualization is permitted
    [22:33:13] <zygoloid> you need launder if f does in fact do something funny

    Was memcpy angeht: Es war nie beabsichtigt, Nutzer eigene Varianten davon definieren zu lassen (die Fußnoten in [basic.types], die memcpy lediglich als Beispiel betiteln, sind insofern irreführend).



  • Arcoth schrieb:

    * Was ist mit Sachen wie

    // [..]
    RefCountedWString* const rcws = static_cast<RefCountedWString*>(malloc(sizeof(RefCountedWString) + sizeof(wchar_t) * length));
    if (!rcws)
      return 0;
    rcws->length = length;
    

    Das war schon immer illegal. malloc erzeugt nämlich keine Objekte. Wird auch als drafting note in P0137 erwähnt:

    Drafting note: this maintains the status quo that malloc alone is not sufficient to create an object.

    Also das letzte mal als ich mich in die Untiefen des Standards (C++98) vorgekämpft habe, hatte ich entschieden den Eindruck dass PODs sich quasi selbst erzeugen, indem man sie einfach in den Speicher schreibt. Was mMn. auch "Memberweise" erlaubt sein müsste, und auf jeden Fall mit memcpy.

    Ist aber im Endeffekt egal. Man kann das ganze auch ganz ohne struct schreiben, dann muss man bloss etwas mehr Zeigerarithmetik mit Hand machen.

    Arcoth schrieb:

    Aber selbst wenn wir das mal übersehen, und davon ausgehen dass jemand dort ein placement new reinhaut, sticht immer noch

    RefCountedWString* const rcws = reinterpret_cast<RefCountedWString*>(reinterpret_cast<char*>(s) - (sizeof(size_t) * 2));
    

    ins Auge. Das wird definitiv nicht mehr wohldefiniert sein.

    Tja, doof. Gibt nämlich wie gesagt definitiv Code der genau das macht.

    Arcoth schrieb:

    Arrays-of-arrays sind in bestimmten Bereichen oft zu finden, und genau so oft findet man da vermutlich Code der dann mit Funktionen draufkloppt die nur "einfache" Arrays unterstützen.

    Das ist an sich kein Problem, da multidimensionale Arrays sowieso nur Syntaxzucker sind; wir können sie einfach durch ein Template simulieren.

    Es geht nicht darum was man mit welchen Mitteln wie umschreiben könnte, damit es den neuen Regeln entspricht. Es geht um die Menge an bestehendem Code der dadurch ungültig wird. Und darum dass das vermutlich alles "stille" Fehler sind, also welche wo der Compiler nix meldet, bzw. nur wenn man Glück hat und er sieht dass man 'was falsch macht.
    Das ist schlimm, das kann man nicht einfach ignorieren.

    Arcoth schrieb:

    Was memcpy angeht: Es war nie beabsichtigt, Nutzer eigene Varianten davon definieren zu lassen (die Fußnoten in [basic.types], die memcpy lediglich als Beispiel betiteln, sind insofern irreführend).

    Ich müsste mir die entsprechenden Stellen nochmal raussuchen, aber was ich mich erinnere ist es im Standard ziemlich eindeutig erlaubt eingebaute Typen sowie PODs zu erzeugen indem man sie Byteweise (per char/unsigned char) schreibt.
    Zumindest ist es eindeutig erlaubt beliebige Speicherbereiche, die beliebige Objekte enthalten dürfen, per char*/unsigned char* zu überschreiben. Und durch die strict-Aliasing Ausnahme für char/unsigned char ist es, nachdem man vollständig mit char/unsigned drübergeschrieben hat, mMn. auch erlaubt den überschriebenen Speicherbereich in einen POD "umzuwidmen" indem man ihn einfach als POD liest.

    Zumindest hab' ich das damals so verstanden.

    Ich bin mir da also nicht so sicher ob das "nie beabsichtigt" war. Wie kommst du zu der Auffassung? Gibt es dementsprechende Statements von Stroustrup oder anderen die am 1. C++ Standard mitgewirkt haben?

    ps: Der VTable Load ist AFAIK nicht wirklich das grosse Problem. Das Problem ist dass hier 3 jeweils vom Vorgänger abhängige Loads in Folge passieren. 1) VTable 2) Function-Pointer 3) Function-Code.
    Wenn du (1) durch einen Vergleich ersetzt ob der VTable eh immer noch der selbe ist, sollte das ganze um einiges schneller werden.
    Also quasi

    for (...)
    {
       if (obj->vtable == vt)       // cmp reg, [reg] + jne else
           funptr();                // call reg
       else
       {
          vt = obj->vtable;
          funptr = vt->someSlot;
       }
    }
    

    Natürlich wäre es besser es ganz wegzuoptimieren, aber der Unterschied zwischen dem was mit diesen neuen Regeln möglich wäre, und dem was so schon geht, ist glaube ich nicht so besonders gross.


  • Mod

    Also das letzte mal als ich mich in die Untiefen des Standards (C++98) vorgekämpft habe, hatte ich entschieden den Eindruck dass PODs sich quasi selbst erzeugen, indem man sie einfach in den Speicher schreibt.

    Nein, du hast wohl den Paragraphen in [basic.life] gesehen, der sagt, dass Objekte mit vacuous initialization nach Allokation ihres Speichers zu leben beginnen. Das haben schon einige falsch interpretiert. Es bezieht sich auf deklarierte Objekte; denn, wie gesagt, malloc allein erzeugt kein Objekt (und dieses hätte auch keinen Initializer).

    Ich müsste mir die entsprechenden Stellen nochmal raussuchen, aber was ich mich erinnere ist es im Standard ziemlich eindeutig erlaubt eingebaute Typen sowie PODs zu erzeugen indem man sie Byteweise (per char/unsigned char) schreibt.

    Natürlich. Mittels memcpy . Das hat mit Iteration mittels char* wenig zu tun.

    Natürlich wäre es besser es ganz wegzuoptimieren, aber der Unterschied zwischen dem was mit diesen neuen Regeln möglich wäre, und dem was so schon geht, ist glaube ich nicht so besonders gross.

    Doch, mir scheint, da ist eine signifikante Lücke. Du musst in jedem Schleifendurchlauf einen load und eine branch extra ausführen. Die Branch wird natürlich quasi nie mispredicted, aber

    hustbaer schrieb:

    Arcoth schrieb:

    Aber selbst wenn wir das mal übersehen, und davon ausgehen dass jemand dort ein placement new reinhaut, sticht immer noch

    RefCountedWString* const rcws = reinterpret_cast<RefCountedWString*>(reinterpret_cast<char*>(s) - (sizeof(size_t) * 2));
    

    ins Auge. Das wird definitiv nicht mehr wohldefiniert sein.

    Tja, doof. Gibt nämlich wie gesagt definitiv Code der genau das macht.

    Hoffen wir mal, dass er weniger in sicherheitskritischen Programmen vorkommt. 😉



  • n3337, [basic.life] schrieb:

    The lifetime of an object of type T begins when:
    — storage with the proper alignment and size for type T is obtained, and
    — if the object has non-trivial initialization, its initialization is complete.

    Da steht "storage is obtained". Wenn ich das falsch interpretiert habe, dann würde ich a) anraten den entsprechenden Absatz neu zu formulieren und b) würde mich interessieren aus welchen Stellen des Standards das klar hervorgeht.

    Arcoth schrieb:

    Ich müsste mir die entsprechenden Stellen nochmal raussuchen, aber was ich mich erinnere ist es im Standard ziemlich eindeutig erlaubt eingebaute Typen sowie PODs zu erzeugen indem man sie Byteweise (per char/unsigned char) schreibt.

    Natürlich. Mittels memcpy . Das hat mit Iteration mittels char* wenig zu tun.

    Also ich kann im Standard quasi nix zum Thema memcpy finden (hab hier nur n3337 zur Verfügung, aber ich hab ja glaub ich auch schon geschrieben dass ich diesbezüglich nur ältere Versionen kenne).
    Wird 1x kurz in [basic.types] erwähnt.

    n3337, [basic.types] schrieb:

    For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object
    holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array
    of char or unsigned char. [40] If the content of the array of char or unsigned char is copied back into the
    object, the object shall subsequently hold its original value.

    Und dort steht in Fussnote [40] explicit "for example". Die Stelle auf die du dich glaube ich schon bezogen hast. Für mich ist die Sache aber ziemlich klar, wenn da steht "bytes kopieren, z.b. mit memcpy oder memmove", dann heisst das ganz sicher nicht dass nur memcpy oder memmove erlaubt sind.

    Wenn du anderer Meinung bist, dann bitte begründe das.

    Also nochmal, was für mich ganz klar ist, ist das folgendes OK ist:

    struct POD
    {
        int x;
    };
    
    void PodConsumer(POD*);
    
    void Foo()
    {
        POD* pod = malloc(sizeof(POD));
        if (!pod)
            exit(1);
        pod->x = 42;
        PodConsumer(pod);
    }
    

    Wenn das nicht gegeben wäre, dann wäre die C-Kompatibilität von C++ gleich 0. Und tonnenweise C++ Code falsch. Da helfen auch Smileys und "hoffentlich nicht sicherheitskritisch" nix.

    Ob hierbei jemals irgendwie ein "POD" Objekt entsteht, oder nur ein "int" Objekt, ist letztendlich egal. Wichtig ist dass es legal ist und das definierte Verhalten das ist was sich jeder mir bekannte C Programmierer erwartet wenn er diesen Code liest.

    ps:
    Falls du dich auf [intro.object] "An object is created by a definition (3.1), by a new-expression (5.3.4)
    or by the implementation (12.2) when needed." beziehst...
    Diese Liste kann mMn. nur unvollständig sein. Dann würde nämlich auch malloc + memcpy/memset nicht ausreichen.


  • Mod

    hustbaer schrieb:

    n3337, [basic.life] schrieb:

    The lifetime of an object of type T begins when:
    — storage with the proper alignment and size for type T is obtained, and
    — if the object has non-trivial initialization, its initialization is complete.

    Da steht "storage is obtained". Wenn ich das falsch interpretiert habe, dann würde ich a) anraten den entsprechenden Absatz neu zu formulieren und b) würde mich interessieren aus welchen Stellen des Standards das klar hervorgeht.

    Das geht klar aus dem wording hervor. Ich habe das auch schon auf Stackoverflow angesprochen. Allerdings bist du bei weitem nicht der erste, der diesen Paragraphen misinterpretiert; vielleicht sollte man mal eine Notiz einfuegen.

    Arcoth schrieb:

    Ich müsste mir die entsprechenden Stellen nochmal raussuchen, aber was ich mich erinnere ist es im Standard ziemlich eindeutig erlaubt eingebaute Typen sowie PODs zu erzeugen indem man sie Byteweise (per char/unsigned char) schreibt.

    Natürlich. Mittels memcpy . Das hat mit Iteration mittels char* wenig zu tun.

    Also ich kann im Standard quasi nix zum Thema memcpy finden (hab hier nur n3337 zur Verfügung, aber ich hab ja glaub ich auch schon geschrieben dass ich diesbezüglich nur ältere Versionen kenne).
    Wird 1x kurz in [basic.types] erwähnt.

    n3337, [basic.types] schrieb:

    For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object
    holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array
    of char or unsigned char. [40] If the content of the array of char or unsigned char is copied back into the
    object, the object shall subsequently hold its original value.

    Und dort steht in Fussnote [40] explicit "for example". Die Stelle auf die du dich glaube ich schon bezogen hast. Für mich ist die Sache aber ziemlich klar, wenn da steht "bytes kopieren, z.b. mit memcpy oder memmove", dann heisst das ganz sicher nicht dass nur memcpy oder memmove erlaubt sind.

    Wenn du anderer Meinung bist, dann bitte begründe das.

    Ich habe mit dem project editor gesprochen. Dem Autor von P0137 und dutzenden anderer Paper, einem der wichtigsten CWG Mitglieder. Der hat unmissverstaendlich erklaert, dass es noch nie wohldefiniert war, mittels char* ueber etwas zu iterieren, was kein Array ist. Und dass die Fussnote, auf die ich mich in meinem letzten Beitrag auch schon bezogen habe, nicht normativ ist, und irrefuehrend, weil sie genau deine Schlussfolgerung suggeriert.

    Also nochmal, was für mich ganz klar ist, ist das folgendes OK ist:

    struct POD
    {
        int x;
    };
    
    void PodConsumer(POD*);
    
    void Foo()
    {
        POD* pod = malloc(sizeof(POD));
        if (!pod)
            exit(1);
        pod->x = 42;
        PodConsumer(pod);
    }
    

    Wenn das nicht gegeben wäre, dann wäre die C-Kompatibilität von C++ gleich 0. Und tonnenweise C++ Code falsch. Da helfen auch Smileys und "hoffentlich nicht sicherheitskritisch" nix.

    Du hast Recht. Millionen LOC waehren dahin. Daher werden diese Regeln sehr wahrscheinlich auch angepasst. 👍

    Ob hierbei jemals irgendwie ein "POD" Objekt entsteht, oder nur ein "int" Objekt, ist letztendlich egal. Wichtig ist dass es legal ist und das definierte Verhalten das ist was sich jeder mir bekannte C Programmierer erwartet wenn er diesen Code liest.

    Warum muss C++ eine so strikte Obermenge von C sein?!

    Falls du dich auf [intro.object] "An object is created by a definition (3.1), by a new-expression (5.3.4)
    or by the implementation (12.2) when needed." beziehst...
    Diese Liste kann mMn. nur unvollständig sein. Dann würde nämlich auch malloc + memcpy/memset nicht ausreichen.

    Liste mal auf, was da deines Erachtens nach noch reingehoert.

    PS: Hol dir mal N4640. Was ist daran so schwer, ein PDF zu downloaden?


  • Mod

    Das wird immer wirrer. Ich bekomme den Eindruck, dass hier etwas in den Standard(entwurf) eingefügt wurde und man überlegt sich erst hinterher, was das eigentlich bedeuten soll. Hier haben wir etwas, dass bestehenden (historisch erwartbar korrekten) Code als undefiniert erklärt, ohne dass es Möglichkeiten gäbe, derartige Stellen sicher automatisiert aufzufinden (wenn der Compiler das könnte, wäre launder ja überflüssig). Gerade in einem solchen Fall erwarte ich doch eine Demonstration, dass es diese Änderung tatsächlich Wert ist (Erfahrung mit einer entsprechenden Implementation, Benchmarks an real-world Code; Belege, dass der entsprechende Performancegewinn nur so erreicht werden kann).
    Andernfalls fürchte ich, dass das zum export von C++17 wird: entweder implementiert kein Compiler die Regln, oder bestehende Projekte werden einfach nicht portiert.

    Die Behauptung, man könnte (bisher) nicht per char* über beliebige Objekte iterieren ist doch sehr gewagt: damit würden [intro.memory], [basic.types](object repräsentation) und [basic.lval] (char aliasing) gegenstandslos.


  • Mod

    Hast du den ersten Paragraphen meiner Antwort zu deinem ersten Beitrag gelesen? Es ist erwartet, dass diese Regeln zu strikt sind. C++ bewegt sich gerade aus dem Moor vager Regelsaetze heraus, und das beinhaltet nunmal, dass ploetzlich Regeln klar dargelegt werden, die nie befolgt wurden.

    Und du musst mir schon erklaeren, inwiefern [intro.memory] gegenstandslos wird. Oder Objektrepraesentierung. Nur weil ich nicht mittels eines char s iterieren kann, heisst das nicht, dass ich nicht das Objekt mittels memcpy in ein char Array kopieren und dieses inspizieren kann.


  • Mod

    Arcoth schrieb:

    Hast du den ersten Paragraphen meiner Antwort zu deinem ersten Beitrag gelesen?

    Ja, hatte es für einen Scherz gehalten.

    Arcoth schrieb:

    Es ist erwartet, dass diese Regeln zu strikt sind.

    Wäre das Wikipedia, würde ich diesen Satz mit einem [Who?] markieren. Kann ja eigentlich nicht der Author sein.

    Arcoth schrieb:

    C++ bewegt sich gerade aus dem Moor vager Regelsaetze heraus, und das beinhaltet nunmal, dass ploetzlich Regeln klar dargelegt werden, die nie befolgt wurden.

    Niemand hat etwas gegen klare Sprache, aber die ist kein Selbstzweck und der Standard (hoffentlich) keine Spielwiese. Dass da schon Regeln waren, die regelmäßig gebrochen wurden, sollte demonstriert und nicht bloß behauptet werden. Und da genügt es nicht, Regeln zu finden, die man nur mit viel Mühe so auslegen kann. Auch gelebte Praxis hat einen gewissen normativen Wert.

    Arcoth schrieb:

    Und du musst mir schon erklaeren, inwiefern [intro.memory] gegenstandslos wird. Oder Objektrepraesentierung. Nur weil ich nicht mittels eines char s iterieren kann, heisst das nicht, dass ich nicht das Objekt mittels memcpy in ein char Array kopieren und dieses inspizieren kann.

    Weil es das Pferd von hinten aufzäumt. Zu Aliasing sagst du ja schon nichts. Und wenn memcpy und memmove die einzigen Funktionen sind, die das erlauben, dann sind diese Abschnitte nicht Teil des Sprachkerns, sondern bloß Magie der Standardbibliothek und systematisch an völlig falscher Stelle befindlich. Wäre auch intressant herauszufinden, ob Kernighan auch der Ansicht ist, dass memcpy etwas Magisches macht, dass nicht auch auf anderem Wege erreicht werden kann.


  • Mod

    Merkwürdig, es gibt ein core issue zu diesem Thema:

    <a href= schrieb:

    CWG 1314">According to 3.9 [basic.types] paragraph 4,

    The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T , where N equals sizeof(T) .

    and 1.8 [intro.object] paragraph 5,

    An object of trivially copyable or standard-layout type (3.9 [basic.types]) shall occupy contiguous bytes of storage.

    Do these passages make pointer arithmetic (5.7 [expr.add] paragraph 5) within a standard-layout object well-defined (e.g., for writing one's own version of memcpy?

    Rationale (August, 2011):
    The current wording is sufficiently clear that this usage is permitted.

    Interessant. Ich sehe nämlich nicht, inwiefern das wording diese Nutzung erlaubt, aber ich denke, die Intention ist schon, das zu erlauben. Ich werde den project editor wohl heute Abend danach fragen.

    Niemand hat etwas gegen klare Sprache, aber die ist kein Selbstzweck und der Standard (hoffentlich) keine Spielwiese. Dass da schon Regeln waren, die regelmäßig gebrochen wurden, sollte demonstriert und nicht bloß behauptet werden.

    Dass Zeigerarithmetik nur über Arrays funktioniert, dürfte dir bekannt sein. [expr.add]/5. Was gibt es da zu diskutieren? Aber siehe oben, das scheint wohl irgendwie unvollständig zu sein.

    Und da genügt es nicht, Regeln zu finden, die man nur mit viel Mühe so auslegen kann. Auch gelebte Praxis hat einen gewissen normativen Wert.

    Große Mengen von Code die eine gewisse Regel verletzen, machen es unumgänglich, diese Regel anzupassen. Die Absicht des Komitees war es auch nicht, alle diese Regeln wider aller Umstände umzusetzen. Was ich auch bereits erwähnt habe. Das wird alles abgeschliffen werden.

    Und wenn memcpy und memmove die einzigen Funktionen sind, die das erlauben, dann sind diese Abschnitte nicht Teil des Sprachkerns, sondern bloß Magie der Standardbibliothek und systematisch an völlig falscher Stelle befindlich.

    Das selbe könnte über initializer_list gesagt werden. Nein, ich denke, das würde schon so passen. Ich sehe auch überhaupt keinen Grund für ein eigenes memcpy .


Anmelden zum Antworten