Defaultkonstruktor der Basisklasse hat seltsames Verhalten



  • DeepCopy schrieb:

    Und warum hat dann

    X::i //einen Zufallswert
    

    aber

    Y::i //keinen ???, also 0
    

    Das Y::i == 0 ist, habe ich mehrfach getestet, ist also kein Zufall.

    Die Frage also lautet warum, gibt es ein X::i ein undefiniertes Verhalten, wie schon angemerkt (was ich auch erwartet habe), aber in X::i findet eine Initialisierung statt?

    Wer sagt denn, dass die 0 kein Zufall ist?



  • Badestrand schrieb:

    DeepCopy schrieb:

    Wenn nun kein Default-Konstruktor in der Basis-Klasse X angegeben ist, wird das in der Y-Klasse geerbte Datenelement Y::i mit 0 initialisiert??? <-- Warum?

    Wenn man selbst keinen Konstruktor angibt, generiert der Compiler einen Default-Konstruktor, der alle Member default-initialisiert (die Default-Initialisierung von int ist 0). Schreibst du den Default-Konstruktor selbst, bist du für's initialisieren der Member zuständig - da du das aber nicht machst, hat die Variable einen zufälligen Wert 🙂

    Der implizite Default-Ctor ist nach Standard äquivalent zu einem selbst implementierten Default-Ctor mit leerer Initialisierungsliste und leerem Rumpf.
    Da wird also gar nichts initialisiert.



  • Ohje ohje, da hatte ich aber gewaltiges Fehlwissen 😕



  • Das Wichtige ist, zwischen POD-Typen, wie sie in C vorkommen (reine Sammlung von Daten), und "tatsächlichen" Klassen (mit Konstruktoren, Destruktor, Zuweisungsoperator als Funktionen) zu unterscheiden.



  • Badestrand schrieb:

    DeepCopy schrieb:

    Wenn nun kein Default-Konstruktor in der Basis-Klasse X angegeben ist, wird das in der Y-Klasse geerbte Datenelement Y::i mit 0 initialisiert??? <-- Warum?

    Wenn man selbst keinen Konstruktor angibt, generiert der Compiler einen Default-Konstruktor, der alle Member default-initialisiert (die Default-Initialisierung von int ist 0). Schreibst du den Default-Konstruktor selbst, bist du für's initialisieren der Member zuständig - da du das aber nicht machst, hat die Variable einen zufälligen Wert 🙂

    edit: Stimmt alles nicht!

    Glaube ich nicht wirklich:

    C++ Standard, 12.1.7 schrieb:

    The implicitly-defined default constructor performs the set of initializations of the class that would be performed by a user-written default constructor for that class with an empty mem-initializer-list (12.6.2) and an empty function body.

    Felix

    EDIT: OK, viiiiiiiiiiiiiel zu langsam



  • Also kurz und gut

    Der GCC hat einen Initialisierungs-Bug in der Version 4.3.3, unter der Annahme das sich der MSVC++ korrekt verhält!

    Es ist schon interessant das die Intitialisierung von i erst nach dem Aufruf des Basis-Konstruktors stattfindet, also (vor/beim) beim Aufruf des Abgeleiteten-Konstruktors.

    @Nexus: Das mit dem POD habe ich nicht verstanden, was sind POD's (kann es sein das du PODS meinst) und welche Wirkung hätte denn Y y gegegenüber Y y = Y() bei der Initialisierung der Member?



  • DeepCopy schrieb:

    Der GCC hat einen Initialisierungs-Bug in der Version 4.3.3, unter der Annahme das sich der MSVC++ korrekt verhält!

    Kein Bug, aber es ist nicht garantiert, dass die Basisklasse initialisiert wird.

    DeepCopy schrieb:

    @Nexus: Das mit dem POD habe ich nicht verstanden, was sind POD's und welche Wirkung hätte denn Y y gegegenüber Y y = Y() bei der Initialisierung der Member?

    Bei Y spielt es keine Rolle, da es sich um einen Nicht-POD-Klassentyp handelt (ich verwende im folgenden den Begriff "Klasse" äquivalent zu "Nicht-POD-Klasse" - das ist aber nicht das Gleiche).

    Plain-Old-Data-Typen haben eine etwas spezielle Behandlung, was Initialisierung betrifft (im Vergleich zu Klassen). Sie erfolgt analog zu den skalaren Typen wie int , double , float* .

    // POD-Typ: für Definition siehe meinen Link im ersten Post
    class MyPOD 
    {
        long var;
    };
    
    // Einfache Definition initialisiert Werte NICHT.
    int   a;
    MyPOD b;
    
    // temporäre Ausdrücke mit Klammer (Konstruktor-Syntax, aber
    // kein Funktionsaufruf im Spiel) initialisieren Werte.
    int();
    MyPOD();
    
    // Deklaration und Initialisierung mit Kopie initialisiert Werte.
    int   c = int();
    MyPOD d = MyPOD();
    

    Das gleiche Verhalten hast du beim Initialisieren der Basisklasse.

    // Klassentyp
    class Derived : public MyPOD 
    {
    };
    
    // Leerer Konstruktorrumpf: Basisklasse MyPOD muss nicht initialisiert
    // werden, wenn sie ein POD ist. Falls die Basisklasse eine Nicht-POD-Klasse
    // ist, wird der Standardkonstruktor aufgerufen. Dass der GCC hier auch bei
    // PODs eine Initialisierung durchführt, ist implementierungsabhängig.
    Derived::Derived()
    {
    }
    
    // Explizite Initialisierung: Werte werden initialisiert.
    Derived::Derived()
    : MyPOD()
    {
    }
    


  • Nexus schrieb:

    Darauf kannst du dich nicht verlassen. Auf MSVC++ sind wie erwartet sämtliche Werte undefiniert.

    Meine Theorie: Der GCC implementiert den impliziten Basiskonstruktoraufruf so:

    Y() : X()
    {
       // ...
    }
    

    Verstehe ich nicht so ganz. Was ändert das denn?



  • Tachyon schrieb:

    Verstehe ich nicht so ganz. Was ändert das denn?

    Es gibt keinen Standardkonstruktor für PODs, folglich muss ein leerer Konstruktor der abgeleiteten Klasse keine Initialisierung der Basisklasse vornehmen, wenn dies nicht explizit getan wird.

    Sonst steht in meinem oberen Post noch einiges...



  • Nexus schrieb:

    Der GCC hat einen Initialisierungs-Bug in der Version 4.3.3, unter der Annahme das sich der MSVC++ korrekt verhält!
    Kein Bug, aber es ist nicht garantiert, dass die Basisklasse initialisiert wird.

    Wie ich es schon mehrfach in vorherigen Posts gesagt habe, erwarte ich KEINE implizite Ininitalisierung meiner Member, insofern sehe ich die ungewollte Inititalisierung als einen GCC-Bug an.

    Ich habe das mit den PODS verstanden explizit auch das meine Basis-Klasse ein POD-Typ ist (ähnlich anonymer union), was aber den Compiler nicht veranlassen sollte meine Member zu Inititalisieren.

    Daher habe ich einen Test gemacht und die Funktion X::foo() als virtual deklariert und ich kann mir ein Schmunzeln nicht verkneifen.

    Seht selbst:
    Geänderte Klasse X

    class X
    {
        public:
         int i;
         /** foo() jetzt virtual - und Klasse X kein POD-Typ mehr! **/
         virtual void foo() { cout << "X::foo() -> X::i: " << i << endl; }
    };
    

    Der Aufruf:

    /** aufruf basis ohne eigenen default construktor **/
        cout << "X ohne eigenen Konstruktor" << endl;
        X x;
        X* px = &x;
        px->foo();
    
        Y y;
        Y* py = &y;
        py->foo();
    
        /** aufruf basis mit eigenem default construktor **/
        cout << endl << "XA mit eigenem Konstruktor" << endl;
        XA xa;
        XA* pxa = &xa;
        pxa->foo();
    
        cout << endl;
    
        YA ya;
        YA* pya = &ya;
        pya->foo();
    

    Die Ausgabe:

    X ohne eigenen Konstruktor
    X::foo() -> X::i: 4196339 <-- hier ist es jetzt richtig
    
    Konstruktor Y ruft geerbtes X::i : 4197680 <-- hier ist es jetzt auch richtig
    Y::foo() -> X::i: 4197680
    
    XA mit eigenem Konstruktor
    Konstruktor XA ruft XA::i : 631681712 <-- hier ist noch richtig
    XA::foo() -> XA::i: 631681712
    
    Konstruktor XA ruft XA::i : 0 <-- aber jetzt kommts...
    Konstruktor YA ruft geerbtes XA::i : 0
    YA::foo() -> XA::i: 0
    

    YA wurde von XA abgeleitet, XA hat einen Konstruktor und ist kein POD-Typ!

    Also wenn das nicht willkürlich ist, was dann...

    @Nexus : In jedem Fall gilt: Vielen Dank für deine Hilfe! 👍



  • Nachtrag @Nexus:

    Ich habe deine Variante:

    #include <iostream>
    
    using namespace std;
    
    class X
    {
        public:
         int i;
           void foo() { cout << "X::foo() -> X::i: " << i << endl; }
    };
    
    int main()
    {
      X x = X();
      return 0;
    }
    

    auf meine Klasse angewandt mit dem folgenden Compiler-Ergebnis:

    main.cpp|14|Fehler: redeclaration of »X x«|
    

    dann kann also nicht sein was du im Post über den impliziten Basiskonstruktoraufruf geschrieben hast:

    Nexus schrieb:

    Meine Theorie: Der GCC implementiert den impliziten Basiskonstruktoraufruf so:

    Y() : X() // <-- X x = X()
    {
       // ...
    }
    

    Und damit fällt natürlich auch der Klassen POD-Type aus.

    Allerdings funktioniert es mit

    int c = int(); // c == 0;
    

    Edit: War alles richtig was du gesagt hast, ich hatte noch eine alte x Definition in der main()-Funktion rumfliegen, sorry!

    Funktioniert ebenfalls!

    X x = X(); // x.i == 0;
    


  • Ist jetzt alles klar, oder ist die Frage bezüglich scheinbar willkürlichem Verhalten von deinem vorletzten Post noch offen?

    Falls noch offen: Kannst du dann nochmals nur den Codeteil, der dir merkwürdig vorkommt, posten? Ich habe ein wenig den Überblick verloren, da XA und YA jetzt ein neues Verhalten aufweisen, aber du keinen neuen Code (Klasse) dazu gezeigt hast...



  • Jetzt ist das mit den POD-Typen und deren speziellem Verhalten klar, danke.

    Zum XA, YA Problem: Ich hatte "NUR" in der X Klasse foo() als virtual deklariert um zu erzwingen das eine:

    Y : X
    

    Vererbung stattfindet und nicht

    Y() : X()
    

    geändert hatte ich sonst nichts.

    Bemerkenswert ist die Tatsache das sich das "default"-Verhalten von Zuständen innerhalb des Compilers herleiten lässt, auf die man als Programmierer keinen Einfluss hat.

    Hier noch einmal der komplette Source (Achtung: foo() ist nicht virtual):

    #include <iostream>
    
    using namespace std;
    /*** basis ohne eigenen default constructor ***/
    class X
    {
        public:
         int i;
    //     virtual void foo() { cout << "X::foo() -> X::i: " << i << endl; }
           void foo() { cout << "X::foo() -> X::i: " << i << endl; }
    };
    
    class Y : public X
    {
        public:
         Y() { cout << "Konstruktor Y ruft geerbtes X::i : " << i << endl; }
         void foo() { cout << "Y::foo() -> X::i: " << i << endl; }
    };
    
    /*** basis mit eigenem default constructor ***/
    class XA
    {
        public:
         int i;
         XA() { cout << "Konstruktor XA ruft XA::i : " << i << endl; }
         void foo() { cout << "XA::foo() -> XA::i: " << i << endl; }
    
    };
    
    class YA : public XA
    {
        public:
         YA() { cout << "Konstruktor YA ruft geerbtes XA::i : " << i << endl; }
         void foo() { cout << "YA::foo() -> XA::i: " << i << endl; }
    };
    
    int main()
    {
        /** aufruf basis ohne eigenen default construktor **/
        cout << "X ohne eigenen Konstruktor" << endl;
        X x;
        X* px = &x;
        px->foo();
    
        cout << endl;
    
        Y y;
        Y* py = &y;
        py->foo();
    
        /** aufruf basis mit eigenem default construktor **/
        cout << endl << "XA mit eigenem Konstruktor" << endl;
        XA xa;
        XA* pxa = &xa;
        pxa->foo();
    
        cout << endl;
    
        YA ya;
        YA* pya = &ya;
        pya->foo();
    
    //    int c = int();
    //    cout << "int c = int(), c: " << c << endl;
    
        X xx = X(); // <-- Dieser Aufruf ist auch als nicht POD-Type möglich, warum?
    
        cout << endl << "X x = X(), x.i: " << xx.i << endl;
        X xxx;
        cout << endl << "X x, x.i: " << xxx.i << endl;
    
        return 0;
    }
    

    Und das überraschende Output von heute (ohne virtuelles foo()):

    X ohne eigenen Konstruktor
    X::foo() -> X::i: 0 <-- hat sich gegenüber letztem build geändert
    
    Konstruktor Y ruft geerbtes X::i : 477025904 <- ab hier wieder richtig
    Y::foo() -> X::i: 477025904 
    
    XA mit eigenem Konstruktor
    Konstruktor XA ruft XA::i : 625056888
    XA::foo() -> XA::i: 625056888
    
    Konstruktor XA ruft XA::i : 0 <-- hat sich gegenüber letztem build geändert
    Konstruktor YA ruft geerbtes XA::i : 0 <-- hat sich gegenüber letztem build geändert
    YA::foo() -> XA::i: 0
    
    X x = X(), x.i: 0 <-- Ok, ist POD-Typ 
    
    X x, x.i: 625056576 <-- richtiges verhalten
    

    Und dasselbe Output von heute (mit virtuellem foo()):

    X ohne eigenen Konstruktor
    X::foo() -> X::i: 4197840 <-- hier scheint es jetzt richtig (nicht POD-Type verhalten)
    
    Konstruktor Y ruft geerbtes X::i : 4198022
    Y::foo() -> X::i: 4198022
    
    XA mit eigenem Konstruktor
    Konstruktor XA ruft XA::i : 0 <-- sollte undefiniert sein (hat c_tor)
    XA::foo() -> XA::i: 0
    
    Konstruktor XA ruft XA::i : -391481744 <-- ab hier wieder richtig, warum???
    Konstruktor YA ruft geerbtes XA::i : -391481744
    YA::foo() -> XA::i: -391481744
    
    X x = X(), x.i: 0 <-- jetzt kein POD-Typ laut Definition, warum ist POD-Type Initialisierung möglich?
    
    X x, x.i: 65535 <-- richtiges verhalten
    

    Wie man sehen kann hat, verhält sich die Initialisierung Willkürlich, und ich meine Willkürlich, mal habe ich X::foo() ohne virtual übersetzt dann wieder mit, mal waren nur die Elemente der Objektinstanz XA mit 0 initialisiert, mal nur die Elemente der Objektinstanz YA, obwohl diese mit X nichts zu tun haben 🤡

    Ob X ein POD-Type ist oder nicht scheint bei der Initialisierung ebenfalls völlig belanglos zu sein.

    Ich habe mal die Nacht darüber geschlafen und bin zu dem Schluss gekommen das der Compiler-Hersteller die Definition "Nicht Initialisiert" == "Undefiniert" == "Willkürlich" gesetzt hat, und somit es dem GCC Compiler freisteht bei "Undefiniertem Verhalten" zu initialisieren oder auch nicht.

    Fazit: Mit den C++ Compiler Herstellern ist es wie mit der Politik - sie können viele Standards versprechen, müssen sich aber letztendlich nicht wirklich daran halten.


  • Mod

    DeepCopy schrieb:

    Wie man sehen kann hat, verhält sich die Initialisierung Willkürlich, und ich meine Willkürlich, mal habe ich X::foo() ohne virtual übersetzt dann wieder mit, mal waren nur die Elemente der Objektinstanz XA mit 0 initialisiert, mal nur die Elemente der Objektinstanz YA, obwohl diese mit X nichts zu tun haben 🤡

    Wie kommst du darauf, dass der Wert 0 zwingend die Folge einer Initialisierung dieses Objektes ist?

    "indeterminate" heißt eben nicht: "irgendein von 0 verschiedener zufälliger Wert",
    sondern "irgendein Wert". Dabei kann dieser Wert in Abhängigkeit von der Implementation durchaus wohlbestimmt sein.



  • Gut, da diese Antwort jetzt schon öfters vorgekommen ist, wie hoch schätzt du die Wahrscheinlichkeit ein, bei 2^31 Möglichkeiten immer wieder (beim Rebuild und Neustart) eine 0 (an festen aber irgendwie willkürlichen Positionen) in deinen Daten-Membern zu erhalten, ein aktives BS vorausgesetzt.

    Es sollte kein "Zufall" sein, genauso wenig wie 10 mal 6 Richtige im Lotto hintereinander.

    camper schrieb:

    ...Dabei kann dieser Wert in Abhängigkeit von der Implementation durchaus wohlbestimmt sein.

    Heißt dann "indeterminate" ? "Mal Vorbestimmt" : "Mal Nicht Vorbestimmt"



  • DeepCopy schrieb:

    Gut, da diese Antwort jetzt schon öfters vorgekommen ist, wie hoch schätzt du die Wahrscheinlichkeit ein, bei 2^31 Möglichkeiten immer wieder (beim Rebuild und Neustart) eine 0 (je nach Wetter und Wind) in deinen Daten-Membern zu erhalten, ein aktives BS vorausgesetzt.

    Es kann unmöglich "Zufall" sein, genauso wenig wie 10 mal 6 Richtige im Lotto hintereinander.

    Es hat auch niemand von Zufall gesprochen. Das warst du alleine. VC macht bei den Debug Builds, wenn keinen explizite Initialisierung statt findet eine 0 Initialisierung. Bei Release sind das dann zufällige Zahlen.



  • DeepCopy schrieb:

    Es kann unmöglich "Zufall" sein, genauso wenig wie 10 mal 6 Richtige im Lotto hintereinander.

    Du vergisst hier einen sehr entscheidenden Punkt: Nicht initialisierte Werte sind nicht zufällig, nicht einmal pseudo-zufällig. Sie entsprechen wichtigen Kriterien wie Gleichverteilung und geringe Korrelation voneinander nicht.

    Es spielt eine Rolle, was vorher an dieser Stelle im Speicher gestanden hat. Da 0 ein sehr oft vorkommender Wert ist (gerade bei Default-Initialisierung), beträgt die Wahrscheinlichkeit durchaus mehr als 2^-31.


  • Mod

    DeepCopy schrieb:

    Gut, da diese Antwort jetzt schon öfters vorgekommen ist, wie hoch schätzt du die Wahrscheinlichkeit ein, bei 2^31 Möglichkeiten immer wieder (beim Rebuild und Neustart) eine 0 in deinen Daten-Membern zu erhalten, ein aktives BS vorausgesetzt.

    Es kann unmöglich "Zufall" sein, genauso wenig wie 10 mal 6 Richtige im Lotto hintereinander.

    Stimmt, und damit beweist du nur, dass du nicht genau genug gelesen hast.
    Ich schreib es nochmal
    Wie kommst du darauf, dass der Wert 0 zwingend die Folge einer Initialisierung dieses Objektes ist?
    In einem üblichen Programm finden bereits vor Aufruf von main eine Menge Operationen statt. Sicherlich hinterlassen diese Operationen den Stack (in dem dann in main dein Objekte erstellt werden) nicht in einem völlig zufälligen Zustand?



  • Ihr seit aber verdammt schnell, ich hatte meinen Post noch gar nicht fertig geschrieben.

    Aber Ok: Ich weiß worauf Ihr hinaus wollt, das es durch aus sein kann das an betimmten Positionen im Speicher 0 initialisierungen Vorliegen können. Auf diese verweisen dann die Datenelemente der Klasse.

    Das habe ich mir auch schon so gedacht, aber dieses Verhalten lässt sich reproduzieren, und das macht mir Kopfzerbrechen.

    In erster Linie ging es mir auch um das Problem das Nexus mir mit den PODS vermittelt hat, und dieses Verhalten scheint mir mal gegeben zu sein und mal nicht.

    camper schrieb:

    Stimmt, und damit beweist du nur, dass du nicht genau genug gelesen hast.

    Da hast du recht, das hatte ich in der Tat, danke für deinen Hinweis, sorry falls es so aussah als wollte ich dich kritisieren.



  • Ich denke das Problem war tatsächlich der "Pseudo"-Zufällige Speicher, nach dem nochmaligen Eruieren des Outputs und unter Beachtung des bisherig erlerntem kann man fast mit Sicherheit sagen das es sich um das angesprochene Phänomen handelt.

    Ich bedanke mich bei Allen die zur Lösung beigetragen haben (besonders Nexus u. camper). 👍


Anmelden zum Antworten