MySQL++, vererbung von std::vector



  • Man sagt ja immer, man solle nicht von std::vector erben, ist das bei MySQL++ eine Ausnahme?

    class MYSQLPP_EXPORT StoreQueryResult :
    		public ResultBase,
    		public std::vector<Row>
    {
        // ...
    };
    


  • Das sagt man weil z.B. std::vector<..> nicht als Basisklasse vorgesehen ist. Technisch spricht nichts dagegen, WENN man sich an die Randbedingungen hält, die sich daraus ergeben.

    Dagegen spricht aber eben z.B. Wartungsfreundlichkeit: das Risiko der fehlerhaften Verwendung ist z.B. grösser weil eben die Randbedingungen nicht eingehalten werden.



  • Wäre dann eine protected-Vererbung nicht angebrachter? Man müsste halt per "using" die Methoden public machen, aber es bestehe kein potentieller Speicherleck. Oder ggf. ein Wrapper?



  • Solange keine (Basisklassen-)Zeiger im Spiel sind, ist das überhaupt kein Problem.



  • Sone schrieb:

    ...

    Warum zitierst Du mich?



  • Editiert.



  • MySQL-Fragensteller schrieb:

    Wäre dann eine protected-Vererbung nicht angebrachter? Man müsste halt per "using" die Methoden public machen

    Warum überhaupt erben? Wenn der einzige Grund, warum man von vector erben will ist, dass man ein paar der Methoden für seine eigene Klasse weiterverwenden will, dann ist das ein schlechter Grund, nämlich einfach Faulheit.
    Dann stattdessen protected Verwerbung und using-Direktiven zu nutzen ist völlig sinnfrei, da kann man den vector gleich als Member verwenden und die entsprechenden Methoden an ihn weiterleiten.



  • Ich denke, die Frage war eher gemeint, ob man einer API vertrauen kann, die von STL-Containern erbt, oder ob da evtl noch viel gröbere "Fehler" vorhanden sind.



  • Naja, so lange die erbende Klasse keinen Destruktor erfordert, ist es ja nicht tragisch.



  • Ethon schrieb:

    Naja, so lange die erbende Klasse keinen Destruktor erfordert, ist es ja nicht tragisch.

    Hat damit nichts zu tun. Sobald ein Objekt der Klasse über einen vector-Pointer zerstört wird, hat man undefiniertes Verhalten. Davon abgesehen ist es einfach ein Desginfehler, den vector als Basisklasse zu nutzen. Der QueryResult ist ja nicht wirklich ein vector (Vererbung = ist-ein-Beziehung) sondern ist das Ergebnis einer Abfrage, das Daten enthält, die zufälligerweise in einem vector gespeichert wurden. Dass das in einem vector geschieht ist ein Implementierungsdetail, das mit der Klasse selbst nicht viel zu tun hat. Schon allein die Mehrfachvererbung sollte einen hier stutzig machen, die ist nämlich häufig* ein Indiz für verkorkstes Design. Das ist hier relativ eindeutig Vererbung aus Faulheit, anders als vermutlich beim ResultBase.

    ______________
    * Für Haarspalter: häufig heißt nicht immer.



  • pumuckl schrieb:

    Ethon schrieb:

    Naja, so lange die erbende Klasse keinen Destruktor erfordert, ist es ja nicht tragisch.

    Hat damit nichts zu tun. Sobald ein Objekt der Klasse über einen vector-Pointer zerstört wird, hat man undefiniertes Verhalten.

    Wieso hat man das? Ein delete auf einen Basisklassenzeiger ohne virtuellen Destruktor wird nur den Destruktor der Basisklasse aufrufen. Hat die erbende Klasse überhaupt keinen Destruktor, unterscheidet sich das verhalten hier doch nicht und sollte bei jeder möglichen Implementierung (Auch auf Marsmännchen-UFO-Rechnern) identisch sein.



  • Ethon schrieb:

    Hat die erbende Klasse überhaupt keinen Destruktor, unterscheidet sich das verhalten hier doch nicht und sollte bei jeder möglichen Implementierung (Auch auf Marsmännchen-UFO-Rechnern) identisch sein.

    Die erbende Klasse hat aber immer einen Destruktor, und sei es ein compilergenerierter. Wenn die abgeleitete Klasse abgesehen von der Basisklasse nur triviale Member hat, macht das in der Praxis sicherlich keinen Unterschied, dennoch ist das Verhalten undefiniert (undefiniert umfasst auch "funktioniert super"). Sobald aber was nichttriviales drin ist, funktionierts nicht mehr. Und im vorliegenden Fall haben wir Mehrfachvererbung. Hier einen delete auf einen vector<Row> aufzurufen kracht gleich mehrfach:

    1. Der Dtor für StoreQueryResult wird nicht aufgerufen, deshalb genauso wenig der Dtor für QueryBase!
    2. Der vector<Row>* zeigt auf den vector-Teil des StoreQueryResult, der wird, da der vector das zweite geerbte Subobjekt ist, nicht am Anfang des StoreQueryResult liegen (je nach Speicherlayout, das ist nicht so genau definiert, aber die meisten werdens so machen dass der ResultBase zuerst liegt). Damit wird nach dem Aufruf des vector-Dtors die Speicherfreigabe mit einem Pointer aufgerufen, der mitten in den StoreQueryResult zeigt - und das wird mit Sicherheit krachen.

    Ums kurz zu machen: delete auf einen Basisklassenpointer, wenn die Basisklasse einen nichtvirtuellen Dtor hat, ist und bleibt UB. Und UB bleibt das größte Übel das man anstellen kann, ganz egal, ob man sagt "wenn aber dann könnte doch vielleicht..." Vor allem wenn man den Fall ausnutzt in dem es bei der aktuellen Versoin des Lieblingscompilers mit der momentanen Version der Klasse bei geringer Speicherauslastung problemlos läuft.



  • Machen wir doch mal ein Beispiel:

    #include <vector>
    class Foo : public std::vector<int>
    {
        std::string myName;
      public:
        explicit Foo(const std::string& n)
          : myName(n)
        { }
    };
    
    int main()
    {
      std::vector<int>* p = new Foo("Hi");  // erlaubt, da Foo ein std::vector<int> ist
      delete p; // Foo::myName wird hier nicht frei gegeben
    }
    

    Delete sieht hier nur ein std::vector<int> und der Destruktor ist nicht virtuell. Daher wird der Destruktor direkt aufgerufen. Foo hat zwar keinen expliziten Destruktor, aber Foo::myName hat intern sicherlich Speicher reserviert, der frei gegeben werden sollte. Der Speicher bleibt aber als Leak übrig, da das delete ja gar nichts von Foo::myName weiß.

    Wäre der Destruktor von std::vector<int> virtuell, würde das delete zwar auch nichts von Foo::myName wissen, aber er weiß, dass er den Destruktor nicht direkt aufrufen darf, sondern in der Liste der virtuellen Member (die vtable) die Adresse vom abgeleiteten Destruktor aufrufen muss. Das Erzeugen von Foo kümmert sich darum, dass jetzt der Zeiger auf eine Funktion zeigt, die auch Foo::myName aufräumt.



  • C++11 schrieb:

    5.3.5 Delete [expr.delete]
    [...]
    5 In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined.

    Der Verweis auf den Standard hätte IMHO gerne früher kommen können, weil es dann keine Diskussion über hätte/würde/könnte gäbe.

    Wie immer an dieser Stelle der Link: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/
    Dort ist das relevante Dokument: N3337.



  • Furble Wurble schrieb:

    Der Verweis auf den Standard hätte IMHO gerne früher kommen können, weil es dann keine Diskussion über hätte/würde/könnte gäbe.

    Die Frage "warum hat man da UB" zu beantworten "weil der Standard das in §XY sagt", wäre zwar völlig korrekt, aber ebenso unzufriedenstellend gewesen. Nachdem ich geschrieben habe, dass es UB ist, bin ich mal einfach davon ausgegangen, dass Ethon mir das glaubt dass es so ist (laut Standard) und technische Gründe wissen will, statt einfach nur "is halt so".



  • pumuckl schrieb:

    Furble Wurble schrieb:

    Der Verweis auf den Standard hätte IMHO gerne früher kommen können, weil es dann keine Diskussion über hätte/würde/könnte gäbe.

    Die Frage "warum hat man da UB" zu beantworten "weil der Standard das in §XY sagt", wäre zwar völlig korrekt, aber ebenso unzufriedenstellend gewesen. Nachdem ich geschrieben habe, dass es UB ist, bin ich mal einfach davon ausgegangen, dass Ethon mir das glaubt dass es so ist (laut Standard) und technische Gründe wissen will, statt einfach nur "is halt so".

    Vielleicht habe ich Ethons "Wieso hat man das?" falsch interpretiert. Eben dahingehend, dass er dass ausdiskutieren will...

    Evtl. ist es auch langweilig, wenn gleich in der ersten Antwort steht:
    Das ist okay, solange nicht [blablabla]. Letzteres ist nämlich "Undefined Behaviour", s. ISO/IEC 14882:2011 5.3.5/5.

    Mehrere Poster hier wussten genau um die UB. Formulierten aber so, dass der Phantasie noch Raum gelassen wurde.
    Mit dem Ergebnis, dass nun noch ein "Beispiel" mit UB und falscher Beschreibung hier prangt.

    Ich hoffe, dass viele andere hier Deine Aussage "Sobald ein Objekt der Klasse über einen vector-Pointer zerstört wird, hat man undefiniertes Verhalten." zum Anlass genommen haben, mal in den Standard zu schauen und selbst nachzulesen, wie Du zu der Aussage gekommen bist.
    Dann wäre meine "bescheidene Meinung" nämlich eine "Unwesentliche, bescheidene Meinung". 🙂



  • Ich hab es genau so wie pumuckl interpretiert und für die meisten bringt es nichts auf den Standard zu verweisen, weil viele Anfänger den überhaupt nicht lesen können. Natürlich weiß ich nicht, ob OP einer ist.



  • Um das ganz kurz klar zu stellen, ich zitiere den Standard, weil ich allgemein als Fachnaivling und Idiot bezeichnet werde. Um dann gleich zu zeigen, das ich Recht habe, zitiere ich am liebsten den Standard, sonst werde ich ignoriert (denk' ich).



  • Furble Wurble schrieb:

    Mit dem Ergebnis, dass nun noch ein "Beispiel" mit UB und falscher Beschreibung hier prangt.

    Inwiefern ist die Beschreibung falsch?

    Sicher sagt der Standard, dass das Verhalten undefiniert ist. Aber wenn ein Compiler an der Stelle nicht definitiv undefiniertes Verhalten implementiert, ist das doch nicht falsch. Ich habe beschrieben, wie die Compiler das technisch umsetzen, da es mir hilft, zu verstehen, warum der virtuelle Destruktor sein muss.

    Oder ist mir da ein Fehler unterlaufen?



  • ich bins schrieb:

    Aber wenn ein Compiler an der Stelle nicht definitiv undefiniertes Verhalten implementiert, ist das doch nicht falsch.

    Möp. JEDER Compiler "implementiert" an der Stelle definitiv undefiniertes Verhalten. Wirklich. Echt wahr. Es ist nämlich völlig egal, was der Compiler macht, alles was er tut passt auf die Beschreibung "undefiniertes Verhalten". Undefiniertes Verhalten heißt: Da passiert irgendwas. Oder garnichts. Es kann alles passieren. Muss aber nicht. Wenn das Programm ordentlich compiliert, nicht abstürzt, und reproduzierbare Ergebnisse liefert, ist das standardkonform. Wenn der Compiler dir eine fette Warnung in rot auf den bildschirm zaubert, ist das standardkonform. Wenn der Compiler die Zeile einfach ignoriert, auch. Selbst wenn das erstelle Programm heimlich die Internetverbindung nutzt und du 45 Minuten später den Pizzaboten vor der Tür stehen hast mit einer Frutti di Mare, "macht dann 8,50, guten Appetit", dann ist das völlig standardkonform.
    Es kann sogar sein, dass dein Programm wirklich wirklich NIE Probleme macht mit deinem Compiler. Du hast aber keinerlei Garantie. Es kann immer funktionieren, außer bei Vollmond und wenn du grade die neue CD von Metallica exakt um Mitternacht eingelegt hast, dann kommt der Pizzabote eben doch, aber diesmal mit 20 Familienpizzen. Undefiniertes Verhalten eben. Vielleicht kann man damit leben. Aber wenn plötzlich der Kunde mitten in der Nacht anruft "Ich hab mir grade schöne Musik aufgelegt und dein Programm gestartet und plötzlich...", was dann? Wäre schon arg peinlich.


Log in to reply