Warum funktioniert ein leerer Type-Conversion-Operator ?


  • Mod

    struct A{
    ...
    };
    
    struct B{
    ...
    	operator A & (){ return *this; }
    };
    

    Dein Konvertierungsoperator B->A& ruft sich selbst auf, wir haben es hier also mit unbeschränkter Rekursion, folglich undefiniertem Verhalten zu tun.

    0xMöbius schrieb:

    Mal angenommen, es geht um 2 Klassen, die sich nur durch die Länge eines internen Arrays unterscheiden.
    Muss ich für die Konvertierung von B nach A wirklich ein neues A erstellen und füllen (auskommentierter Bereich)?

    ja (wobei die Rückgabe einer Referenz nat. auch fehlerhaft ist). Aliasing (d.h. die Benutzung einer Speicherregion eines bestimmten Typs so als ob diese einen anderen Typ hätte) ist in C++ nur unter sehr eng begrenzten Umständen zulässig.


  • Mod

    camper schrieb:

    unbeschränkter Rekursion, folglich undefiniertem Verhalten zu tun.

    Aus Neugier, ist das zwangsläufig so? [intro.multithread]/27 spricht nicht von UB. Oder führen Rahmenbedingungen, die solche Annahmen nicht erfüllen, zu UB?

    Muss ich für die Konvertierung von B nach A wirklich ein neues A erstellen und füllen (auskommentierter Bereich)?

    Folgendes könnte ggf. für dich interessant sein. Die Klassen haben Standardlayout, demnach greift [class.mem]/19 und

    union {B b; A a;} u{}; // Initialisiert b, welches nun der aktive Member ist
    std::cout << u.a.doubles[0];
    

    ist definiert.



  • Dein Konvertierungsoperator B->A& ruft sich selbst auf, wir haben es hier also mit unbeschränkter Rekursion, folglich undefiniertem Verhalten zu tun.

    Also das hätte ich, wenn ich dich richtig verstehe, mal garnicht erwartet. Wieso ruft der Konvertierungs-OP sich selbst auf? Müsste dann nicht irgendwo ein Funktions-Stack explodieren?
    Nach dem OP ist doch aus dem B ein A geworden, und es gibt keinen Grund ihn nochmal aufzurufen.
    Ich hätte jetzt erwartet, dass der this-Zeiger von B als this-Zeiger von A interpretiert wird. Also quasi ein impliziter reinterpret_cast.. Warum erlaubt der Compiler denn dem Cast-OP ein Typ B zurück zu geben, wo eigentlich ein Typ A erwartet wird, ohne irgendeinen Cast??

    Ich habe jetzt einfach mal den (reinterpret_cast<A>(this)) manuell ausgeführt. Und!? Es geht! Aber wie kann das sein?! 😕

    Vielleicht noch ein kurzer Nachtrag zum kompilierten (gcc) Programm: Ohne Cast, so wie es in meinem vorigen Post steht, stürzt es zwar nicht ab, aber "fkt(b)" gibt auch nichts aus und es wird mit 0xC0000005 (Access Violation) beendet. Hier frage ich mich, warum es nicht gleich abstürzt..
    Naja, mit reinterpret_cast gibt "fkt(b)" aus, was sie soll und das Programm wir normal beendet.
    Macht das irgendwie Sinn (zumindest für den o.g. Fall)?

    ja (wobei die Rückgabe einer Referenz nat. auch fehlerhaft ist).

    Ich hatte da im Hinterkopf, dass die Lebenszeit von Referenzen bei Bedarf automatisch verlängert werden kann..
    Das würde ja bedeuten, dass ich (zumindest temporär) 3 Objekte habe - ein verpacktes, eine neue Kopie und eine Kopie der Kopie. Geht das nicht einfacher?
    Das macht mich irgendwie nicht glücklich (wie in letzter Zeit vieles, dass mit Templates zutun hat, nur so nebenbei..).

    Aliasing (d.h. die Benutzung einer Speicherregion eines bestimmten Typs so als ob diese einen anderen Typ hätte) ist in C++ nur unter sehr eng begrenzten Umständen zulässig.

    Das klingt immerhin nach einem Ausweg, um statt mit Kopien von Kopien doch mit nur einem Speicherbereich arbeiten zu können...
    Was wäre z.B. wenn man tatsächlich die Daten des originalen Speichers ändern möchte (in einer Funktion, die nur mit kleineren Speicherbereichen arbeitet)?! So müsste man erst (mindestens) eine Kopie erstellen, diese ändern, und dann damit den eigentlichen Speicherbereich überschreiben. Das klingt schon wie "von hinten durch die Brust ins Auge"..

    Mal sehen, vielleicht ist es Wert, ein neues Thema zu starten, z.B.: "Klasse als Teilmenge einer anderen Klasse. Wie effizient umwandeln?"
    Also sosusagen soetwas wie die Beziehung von Basis zu Abgeleitetem, nur ohne Vererbung... Bzw. den Cast von Derived->Base manuell nachbilden..


  • Mod

    Müsste dann nicht irgendwo ein Funktions-Stack explodieren?

    Wird er wahrscheinlich auch, wenn du das Programm ausführst. Dann wird der Frame-Stack überlaufen.

    denn dem Cast-OP ein Typ B zurück zu geben, wo eigentlich ein Typ A erwartet wird, ohne irgendeinen Cast??

    Warum? B ist doch nach A& konvertierbar. I.e.

    operator A&() {return *this;}
    

    Ist nach Anwendung des eben deklarierten operator A& äquivalent zu

    operator A&() {return this->operator A&();}
    

    .. ein Bilderbuchbeispiel endloser Rekursion. Clang gibt hier auch eine hübsche Fehlermeldung.

    Ich habe jetzt einfach mal den (reinterpret_cast<A>(this)) manuell ausgeführt. Und!? Es geht! Aber wie kann das sein?!

    Warum sollte es nicht gehen? Wenn wir den Standard pedantisch befolgen hat dein Programm undefiniertes Verhalten. Das schließt jedoch nicht aus, dass das resultierende Verhalten dem Erwarteten gleicht.

    es wird mit 0xC0000005 (Access Violation) beendet.

    Jo. Das heißt Abstürzen. Das Programm ist abgekrazt nachdem ein Frame gepushed wurde, welches auf dem Stack keinen Platz mehr hatte.


  • Mod

    Arcoth schrieb:

    camper schrieb:

    unbeschränkter Rekursion, folglich undefiniertem Verhalten zu tun.

    Aus Neugier, ist das zwangsläufig so? [intro.multithread]/27 spricht nicht von UB. Oder führen Rahmenbedingungen, die solche Annahmen nicht erfüllen, zu UB?

    Solche Spitzfindigkeiten hatte ich nicht im Blick. Zweifellos ist die Rekursion hier nicht völlig leer, weil ja ein Wert produziert werden muss. Wenn also der Compiler die rekursion eliminiert (tail-rekursion oder so) und auch noch die resultierende Schleife, weil sie leer ist... haben wir immer noch nichts in der Hand, um den Rückgabewert zu initialisieren.



  • Arcoth schrieb:

    camper schrieb:

    unbeschränkter Rekursion, folglich undefiniertem Verhalten zu tun.

    Aus Neugier, ist das zwangsläufig so? [intro.multithread]/27 spricht nicht von UB. Oder führen Rahmenbedingungen, die solche Annahmen nicht erfüllen, zu UB?

    Ist es nicht so dass C++ von jedem Programm verlangt "progress" in endlicher Zeit zu machen?
    Also dass jedes Programm das (ab einem gewissen Punkt) *nie* mehr irgendetwas beobachtbares macht UB isthat. Bzw. auch jedes Programm welches einen Thread enthält der nie irgend etwas beobachtbares macht.
    Also der Grund warum

    while (true);
    

    verboten ist.

    Würde das nicht auch eine unendliche Rekursion verbieten die keinen beobachtbaren Effekt hat, so wie in diesem Beispiel?


  • Mod

    Lass mich die Frage anders formulieren: ist for(;;); zwangsläufig UB?

    Ist es nicht so dass C++ von jedem Programm verlangt "progress" in endlicher Zeit zu machen?

    Nein. Der von mir angesprochene Paragraph sagt, dass Implementierungen dies annehmen dürfen. Darin besteht der Unterschied der zu meiner Frage führt.


  • Mod

    Doch, es scheint als ob diese endlose Rekursion tatsächlich undefiniertes Verhalten hat: N1528 erklärt, dass die entsprechende Passage in C - die der in C++ sehr ähnlich ist - endlosen Schleifen undefiniertes Verhalten zuschreibt. Dazu kommt ein Zitat aus einem Artikel dessen Autor mit dem Autor des bereits verlinkten Dokuments kollaboriert hat:

    <a href= schrieb:

    Compilers and Termination Revisited">Unfortunately, the words “undefined behavior” are not used. However, anytime the standard says “the compiler may assume P,” it is implied that a program which has the property not-P has undefined semantics.



  • Ich hatte zwar keine Ahnung dass es N1528 gibt, aber meine Logik sagte mir einfach auch so "implementation may assume" == UB 😉 *
    Weil: ich wüsste nicht was es sonst bedeuten soll.

    Sollte aber vermutlich klargestellt werden. Also an allen Stellen wo "implementation may assume" steht den Text ändern (z.B. NOTE ala "this means programs that do X have undefined bevahior" dazumachen). Oder gleich ganz aufräumen und alle Formulierungen vereinheitlichen, und dann irgendwo 1x an einem zentralen Punkt klarstellen dass die Implementierung annehmen darf dass das Programm kein UB erzeugt.

    EDIT:
    *: "UB wenn der Programm die Annahme nicht erfüllt" ist natürlich gemeint



  • Arcoth schrieb:

    Folgendes könnte ggf. für dich interessant sein. Die Klassen haben Standardlayout, demnach greift [class.mem]/19 und

    union {B b; A a;} u{}; // Initialisiert b, welches nun der aktive Member ist
    std::cout << u.a.doubles[0];
    

    ist definiert.

    Das ist wirklich gut zu wissen. Auf sowas hatte ich gehofft. Mit deinem Code-Schnipsel hast du mich jetzt aber etwas verwirrt. Ne union ist dafür doch nicht zwingend nötig, oder??

    Arcoth schrieb:

    denn dem Cast-OP ein Typ B zurück zu geben, wo eigentlich ein Typ A erwartet wird, ohne irgendeinen Cast??

    Warum? B ist doch nach A& konvertierbar. I.e.

    operator A&() {return *this;}
    

    Ist nach Anwendung des eben deklarierten operator A& äquivalent zu

    operator A&() {return this->operator A&();}
    

    ..

    Ach.. Ich doof!? Bei jeder normal aussehenden Funktion wäre mir das sicher aufgefallen - bestimmt... Den impliziten Cast muss ich wohl übersehen haben.. 🕶
    Ne gemeine Falle, sozusagen mit der Deklaration schon den Cast zu "definieren", und ihn anzuwenden, ohne ihn hinzuschreiben.

    Arcoth schrieb:

    ein Bilderbuchbeispiel endloser Rekursion. Clang gibt hier auch eine hübsche Fehlermeldung.

    Das ist schön - für Clang-Nutzer..

    Arcoth schrieb:

    Ich habe jetzt einfach mal den (reinterpret_cast<A>(this)) manuell ausgeführt. Und!? Es geht! Aber wie kann das sein?!

    Warum sollte es nicht gehen? Wenn wir den Standard pedantisch befolgen hat dein Programm undefiniertes Verhalten. Das schließt jedoch nicht aus, dass das resultierende Verhalten dem Erwarteten gleicht.

    Einer der Aspekte, die mich an c++ besonders stören: Mit Betäubung in den Fuß schießen, und dann irgendwann umbemerkt verbluten.
    Lässt sich der reinterpret_cast und das Standard-Layout der Klasse nicht zu definiertem Verhalten verbinden??
    Mit der union weiß ich nicht genau, wie ich weiter machen soll. Ein Cast des Speicherbereichs wäre das, was ich idealerweise erwarten würde.. Also mal direkt gefragt: reinterpret_cast in Fall von Standard-Layout-Klassen gut oder böse?

    Arcoth schrieb:

    es wird mit 0xC0000005 (Access Violation) beendet.

    Jo. Das heißt Abstürzen. Das Programm ist abgekrazt nachdem ein Frame gepushed wurde, welches auf dem Stack keinen Platz mehr hatte.

    Na dann ists ja gut. Ich habe mich nur gewundert, dass es noch bis zum return der main() kommt.. Gibts dafür auch ne Erklärung, oder kann ich das unter UB verbuchen?! 😉

    Ich will euch ja nicht stören, aber eine kleine Frage ;):

    Arcoth schrieb:

    Doch, es scheint als ob diese endlose Rekursion tatsächlich undefiniertes Verhalten hat

    Endlos, nicht weil "while(true) / for( ;; )", sondern weil kein "break;", richtig?


  • Mod

    Ne union ist dafür doch nicht zwingend nötig, oder??

    Doch.

    Lässt sich der reinterpret_cast und das Standard-Layout der Klasse nicht zu definiertem Verhalten verbinden??

    Solange die Klassen völlig unabhängig sind, nicht.

    Ich habe mich nur gewundert, dass es noch bis zum return der main() kommt..

    Ist es nicht.

    Endlos, nicht weil "while(true) / for( ;; )", sondern weil kein "break;", richtig?

    Korrekt. Ob ein bedingter Abbruch durch die Abbruchbedingung oder ein manuelles if (...) break; stattfindet, ist doch egal.



  • Man braucht soweit ich weiss nichtmal einen Abbruch, die Schleife darf ruhig ewig laufen. So lange sie halt immer wieder beobachtbare Effekte hat.



  • 0xMöbius schrieb:

    Endlos, nicht weil "while(true) / for( ;; )", sondern weil kein "break;", richtig?

    Naja, selbst mit einem break kanns ja endlos sein:

    while(true) { if (always_false()) break; }
    

    hustbaer schrieb:

    Man braucht soweit ich weiss nichtmal einen Abbruch, die Schleife darf ruhig ewig laufen. So lange sie halt immer wieder beobachtbare Effekte hat.

    Was sind jetzt eigentlich genau beobachtbare Effekte? Laut dem Link von Arcoth wären das ja nur (informell):

    • Alles mit volatile
    • IO
    • Synchronization/atomic operations

    Woher weiß der Compiler das, hat der eine Liste mit allen Funktionen die so etwas machen können (die letzen beiden Punkte)?


  • Mod

    Der Compiler muss die Information, dass eines dieser "Events" auftreten muss, nicht nutzen, geschweige denn überprüfen.



  • Arcoth schrieb:

    Der Compiler muss die Information, dass eines dieser "Events" auftreten muss, nicht nutzen, geschweige denn überprüfen.

    Aber der Compiler darf doch nur bei solchen Schleifen annehmen dass sie terminieren, die keine der oben genannten Dinge machen. Also muss er das doch überprüfen?

    Wenn ich schließlich etwas habe wie:

    for (;;)
    {
        do_something();
    }
    

    dann ist das wenn do_something z.B. in eine Datei schreibt doch nicht UB (trotz endlos-Laufzeit) und der Compiler kann die Schleife nicht einfach zusammen-optimieren. Also muss er doch erstmal untersuchen ob do_something etwas macht dass die Schleife "legal-endlos" macht oder nicht?



  • Der Compiler muss bei allem was er nicht "einsehen" kann annehmen, dass es alles macht was es machen kann.
    Er muss immer "pessimistisch" optimieren.

    Wobei man dem Compiler natürlich Listen spendieren könnte, bzw. z.B. die Standard Header Files (des Compilers, des OS', ...) mit diversen non-Standard Pragmas/Attributen/... versehen, die die nötigen Informationen enthalten.

    Wie weit das bereits üblich ist oder nicht kann ich nicht sagen. Beim Windows SDK sind mir bisher auf jeden Fall keine solchen Attribute/... aufgefallen.

    Interessant wären da ja z.B. auch andere Dinge. Wie z.B. ob eine Funktion einen übergebenen Wert "nur verwendet", oder ob sie ihn u.U. irgendwo in eine Datenstruktur reinschreibt, wo ihn andere Funktionen "aufgreifen" könnten.

    Nochmal konkret zu deinem Beispiel: wenn der Compiler den Code von do_something "sehen" kann (inklusive aller aufgerufenen Unterfunktionen und deren Unterfunktionen), und darin keine Beobachtbaren Effekte zu finden sind, dann kann er die Schleife wegoptimieren. Anderenfalls kann er das nicht.


Anmelden zum Antworten