"to MI or not to MI"



  • Expression Templates (CRTP), oder wenn man einfach nur ein Concept (so heisst statisches Interface hoffentlich in C++17) hat, fuer das man default-Implementierungen hat. Oder Policy-Based Class Design. Konkret habe ich es verwendet bei der Implementierung von Vektoren und Matrizen, sowie ein paar kleinen Utilities.

    Uebrigens ist boost::noncopyable ein sehr gutes Beispiel dafuer.



  • Gut werde mal policy design nochmal in Modern C++ nachlesen. Vererbung haette ich trotzdem nicht benutzt.

    Mein konkretes Beispiel: Histogram_match haengt von einem Aehnlichkeitsmass ab, dass benutzt wird (als "Policy", vielleicht ist es auch keine). Da gibt es dann mehrere Umsetzungen:

    template<typename S>
    struct histogram_match
    {
        // map an rgb image to an image where every pixel contains similarity values
        apply(image<rgb> in, image<float> out) const
        {
            S similarity_measure;
        }
    }
    

    oder wenn Zustand gespeichert werden soll:

    template<typename S>
    struct histogram_match
    {
        // map an rgb image to an image where every pixel contains similarity values
        apply(image<rgb> in, image<float> out) const
        {
        }
        private:
            S similarity_measure;
    }
    

    oder

    struct histogram_match
    {
        // map an rgb image to an image where every pixel contains similarity values
        template<typename S>
        apply(image<rgb> in, image<float> out) const
        {
            S similarity_measure;
        }
    }
    

    Aber vielleicht ist das garnicht policy based design.

    Niemals wuerde ich machen:

    struct histogram_match : S
    {
    }
    

    Uebrigens ist boost::noncopyable ein sehr gutes Beispiel dafuer.

    Das genau wieviel Implementierung besitzt? Aber, das akzeptiere ich als einfaches nachvollziehbares Beispiel. Und auch als Beispiel fuer Mehrfachvererbung wenn man andere Eigenschaften wie moveable hinzukommen.



  • knivil schrieb:

    Niemals wuerde ich machen:

    struct histogram_match : S
    {
    }
    

    Damit hast du aber den Vorteil, dass die Policy das Interface erweitern kann.

    knivil schrieb:

    Das genau wieviel Implementierung besitzt? Aber, das akzeptiere ich als einfaches nachvollziehbares Beispiel. Und auch als Beispiel fuer Mehrfachvererbung wenn man andere Eigenschaften wie moveable hinzukommen.

    Hier ein Beispiel aus der realen Welt (das ich in erweiterter Form auch so umgesetzt habe): Du schreibst eine Mathematik-Bibliothek fuer 3D Anwendungen. Da gibt es "reine" Vektoren, Farben, Punkte, etc. Alle von diesen sind aber letztlich irgendwie Vektor-artig. Ausserdem haben sie gemeinsam, dass du gerne Zugriff auf ihre Daten haettest, damit du diese an ein Grafik-API wie OpenGL oder DX weitergeben kannst. Da das ganze effizient sein soll, scheidet Polymorphie schon von vornherein aus, auch will man Value-Semantiken.

    Oft wird dann jede dieser Klassen fuer sich geschrieben, was 2 Nachteile hat:
    - Man sollte das Interface zum Datenzugriff konsistent halten
    - Code Duplikation

    Hier koennte man beispielsweise eine statische Basisklasse vector_storage einfuehren. Diese hat intern einen Array-Member fuer die Daten und stellt den [] Operator sowie .data() bereit. Auch koennte diese Klasse haeufig gebrauchte Operationen implementieren, z.B. Vektoraddition.
    Man leitet einfach vector, color und point von vector_storage ab und loest beide Probleme.



  • Kellerautomat schrieb:

    Da mich das Thema interessiert, moechte ich hier gerne die MI-Diskussion aus diesem Thread fortsetzen: http://www.c-plusplus.net/forum/314127

    Ne, da ist nichts interessantes. Ehrlich nicht.

    Vererbung ist seit etlichen Jahren ziemlich gut verstanden - da gibt es nichts neues mehr.

    Was interessant ist sind Mixins. Das ist ein Thema das noch nicht 100% verstanden ist. Es gibt unterschiedliche Implementierungen und Ansaetze dazu. Wenn dich das Thema also interessiert, schau dir Mixins an.

    In C++ macht man die so: http://www.drdobbs.com/cpp/mixin-based-programming-in-c/184404445

    Dein letztes Beispiel macht uebrigens keinen Sinn. Wenn du hier ableiten willst, dann tue es einfach. Das ist kein neues Konzept oder so: man muss nicht jede Vererbung immer Polymorph nutzen. Logischer waere es aber, nicht zu Vererben sondern einfach vector_storage als member zu haben.

    Denn alles was dir eine Ableitung bringt, ist die schreibarbeit fuer data() und op[] zu ersparen.

    Stell dir mal vor, du willst statt vector_storage eine andere Klasse nehmen. Dann kannst du das in der Vererbungsvariante nicht mehr machen - weil du sonst dein oeffentliches Interface ja anpassen musst. Wenn du aber korrekterweise hier Aggregation verwendet hast, ist es kein Problem - da es nur ein Implementierungsdetail deiner Klasse ist.

    Vererbung will man eigentlich nie haben. Sie ist furchtbar. Manchmal ist sie notwendig, dann muss man sie halt verwenden. Aber es hat seinen Grund warum es Objekt Orientierte Sprachen gibt, die keine Vererbung unterstuetzen. Vererbung ist eigentlich nur fuer eins gut: ueberschreiben von virtuellen Funktionen.

    PS:
    Das Interface einer Klasse lässt sich auch super einfach per freistehenden Funktionen erweitern. Man muss nicht immer die Klasse anfassen nur weil man Funktionalität hinzufügen will.

    Und Templates ermöglichen hier generische Implementierungen.



  • Shade Of Mine schrieb:

    Denn alles was dir eine Ableitung bringt, ist die schreibarbeit fuer data() und op[] zu ersparen.

    Das ist doch das Ziel.

    Shade Of Mine schrieb:

    Stell dir mal vor, du willst statt vector_storage eine andere Klasse nehmen. Dann kannst du das in der Vererbungsvariante nicht mehr machen - weil du sonst dein oeffentliches Interface ja anpassen musst. Wenn du aber korrekterweise hier Aggregation verwendet hast, ist es kein Problem - da es nur ein Implementierungsdetail deiner Klasse ist.

    Dann haelt sich der schreiber dieser Klasse besser an das Concept "Storage".



  • Kellerautomat schrieb:

    Shade Of Mine schrieb:

    Denn alles was dir eine Ableitung bringt, ist die schreibarbeit fuer data() und op[] zu ersparen.

    Das ist doch das Ziel.

    Nein. Schreibarbeit sparen soll dir deine IDE, nicht Vererbung.

    Shade Of Mine schrieb:

    Stell dir mal vor, du willst statt vector_storage eine andere Klasse nehmen. Dann kannst du das in der Vererbungsvariante nicht mehr machen - weil du sonst dein oeffentliches Interface ja anpassen musst. Wenn du aber korrekterweise hier Aggregation verwendet hast, ist es kein Problem - da es nur ein Implementierungsdetail deiner Klasse ist.

    Dann haelt sich der schreiber dieser Klasse besser an das Concept "Storage".

    Den Satz verstehe ich nicht.



  • Shade Of Mine schrieb:

    Vererbung will man eigentlich nie haben. Sie ist furchtbar.

    Das kann ich nicht nachvollziehen, konkretes Beispiel: Im DOM gibt es verschiedene Nodes, da will ich schon haben, dass Element von Node ableitet und nicht dauern mit node.type-switch-Triaden rumeiern.

    Und in diesem Zusammenhang hab ich auch ein gutes Beispiel für MI:
    Node - ChildNode (hat einen parent) - ParentNode (hat eine Liste von children) und dann gibt es eben Nodes wie Element die sowohl ChildNode als auch ParentNode sind.

    Das Diamond-Problem ist in C++ zwar etwas hässlich, aber andere Sprachen kommen damit wesentlich besser klar - Eiffel bspw.

    Ich hab mit Interesse den StackOverflow-Artikel aus dem anderen Thread gelesen der das Problem des physikalischen Speicherlayouts behandelt, das ist zwar ein Punkt, aber MI soll ja auch nicht oft vorkommen, aber immer dann wenn es notwendig ist fehlt sie mir doch.

    Jetzt ist es so, dass ParentNode von ChildNode ableitet um Code-Duplikation zu verhindern, was ziemlich, ziemlich übel ist imho. Vor allem weil nicht jeder ParentNode auch ein ChildNode ist und in manchen Ableitungen überprüft werden muss ob man eh keinen Parent hat, etc. etc.

    Welche Alternativen habe ich in dem Fall? Wie kann ich die Klassen besser darstellen und möglichst Code-Duplikation verhindern, eine sinnvolle Klassenhierarchie haben (kein ParentNode : ChildNode) und trotzdem alles auch als Node verwenden zu können?

    Dinge die ich in objektorientierten Sprachen verbieten würde: protected Member.

    @Shade: Mixins sind sehr interessant, in unserer Firma werden die im C#-Bereich sehr viel eingesetzt und sie sind sehr praktisch. Aber wie du schon gesagt hast, da muss sich imho noch etwas tun bis es da den "the way to go"-WEg gibt.

    MfG SideWinder



  • Shade Of Mine schrieb:

    Nein. Schreibarbeit sparen soll dir deine IDE, nicht Vererbung.

    Unabhängig von der Vererbung sehe ich das nicht so. Ich will mich minimal ausdrücken können, es ist schön und gut, dass mir Eclipse die Getter/Setter in Java erstellt, aber ich hasse Java trotzdem dafür, dass ich mir damit meine Klasse zumüllen muss. Die Lesbarkeit verschlechtert sich furchtbar. Ich will C#-Properties.

    C# ist deswegen eine so viel bessere Sprache (Achtung: Sprache, nicht unbedingt Plattform), weil ich mich wesentlich besser ausdrücken kann.

    MfG SideWinder



  • SideWinder schrieb:

    Unabhängig von der Vererbung sehe ich das nicht so.

    Du wuerdest doch nie auf die idee kommen properties fuer normale Funktionen zu verwenden, weil es weniger Schreibarbeit ist, oder?

    class Foo {
      public void writeFunction(string text) { System.Console.WriteLine(text);
      //vs
      public string writeProperty { set { System.Console.WriteLine(value) } }
    }
    

    Selbes bei Sachen wie Vererbung: vererbung drueckt eine Beziehung aus. Wenn man diese Beziehung will, nimmt man vererbung. Will man sie nicht, nimmt man keine Vererbung.



  • Ich verwende Vererbung nicht deswegen um mir Schreibarbeit zu sparen. Aber ich finde nicht, dass die IDE für das Ersparen von Schreibarbeit zuständig ist sondern in erster Linie die Sprache mit ihrer Ausdruckskraft.

    MfG SideWinder



  • SideWinder schrieb:

    Ich verwende Vererbung nicht deswegen um mir Schreibarbeit zu sparen. Aber ich finde nicht, dass die IDE für das Ersparen von Schreibarbeit zuständig ist sondern in erster Linie die Sprache mit ihrer Ausdruckskraft.

    Ich weiss nicht ob ich das Unterschreiben wuerde.
    Klar ist es praktisch wenn ich mit wenigen Zeichen komplexe Sachen ausdruecken kann - aber worum es geht, ist das was ich Ausdruecke.

    Oft will ich ja viel Schreiben, zB bei Funktionsnamen.

    Schreibarbeit sollte IMHO nie ein Argument sein. zB so Sachen wie Access Specifiers wie private/public sind mehr Schreibarbeit als alles public zu machen.

    Auch sind manche Sprachen generell geschwaetziger als andere.

    Wenn also ein Konzept wie Properties Schreibarbeit erspart ist das nett - aber es sollte nie die Motivation fuer die Verwendung so eines Konzeptes sein.



  • Mhm, ich bin nicht in der Lage mich richtig auszudrücken. Ich meine natürlich mit "Schreibarbeit sparen" nicht Variablennamen i,j,k,l,m,n, etc., die darf mir natürlich die IDE mit CTRL+SPACE verlängern.

    Es geht mir in erster Linie um Boilerplate-Code der meine Klasse vergrößert, verkomplexisiert. Eben 10 Getter statt Properties. Eine 5-Zeilen-For-Schleife die Elemente filtert anstatt result = collection.where(.Value > 10). Solcherlei.

    Die For-Schleife muss sich jemand erst ansehen, nachsehen ob sie eh kein +=2 im Index hat, ob sie eh nicht irgendwo etwas ein klein wenig anders macht. Weil ich nicht ausdrücken kann was ich eigentlich sagen mag: Gib mir nur jene Elemente deren Value größer als 10 ist.

    Edit: Darum geht's hier aber nicht. Würde mich eher freuen wenn jemand etwas zu meinem Posting über MI schreibt 🤡

    MfG SideWinder



  • Kellerautomat schrieb:

    Da mich das Thema interessiert, moechte ich hier gerne die MI-Diskussion aus diesem Thread fortsetzen: http://www.c-plusplus.net/forum/314127

    Einerseitz finde ich Xins Ansatz interessant, rein logisch kann ich dem jedoch (noch) nichts abgewinnen. Darum moechte ich Xin bitten, ein nicht konstruiertes Modell aus realem Code zu zeigen und zu erklaeren.

    Sorry, für dieses eher sinnlose Posting, ich habe den Thread hier gerade erst nach einem Hinweis von Sidewinder gesehen. Durch RudP cruise ich nur bei zuviel Zeit und ich kann jetzt nicht drei Seiten durchhangeln...

    Kellerautomat schrieb:

    Ich bitte darum, die Diskussion konstruktiv fortzusetzen und nicht wieder gegenseitig aufeinander rumzuhacken - das fuehrt zu nichts.

    ...mag mich aber auch keiner Diskussion entziehen, die offen und konstruktiv werden soll. Ich hoffe heute abend nochmal Zeit zu finden und klinke mich dann nochmal ein.



  • SideWinder schrieb:

    [...]

    Und in diesem Zusammenhang hab ich auch ein gutes Beispiel für MI:
    Node - ChildNode (hat einen parent) - ParentNode (hat eine Liste von children) und dann gibt es eben Nodes wie Element die sowohl ChildNode als auch ParentNode sind.

    Das Diamond-Problem ist in C++ zwar etwas hässlich, aber andere Sprachen kommen damit wesentlich besser klar - Eiffel bspw.
    [...]

    MfG SideWinder

    Was genau hat man denn dann eigentlich gewonnen? Nun sind die Objekte zwar in sich klar strukturiert, aber der Code, welcher ein fertiges DOM vorgesetzt bekommt (und das ist ja der einzig relevante Code, für den man die Mühe betreibt, alles klar, leserlich und strukturiert zu machen, als Bibliotheksschreiber), weiß doch immer noch nicht die konkreten Typen und muss trotzdem irgendwie Type-Switching betreiben. Oder sehe ich da was falsch?

    Auf meiner Todo-Liste steht auch bald wieder eine einfache Baumstruktur und ich habe leider noch keine gute Lösung für das Inner-Node/Leaf-Problem gefunden. Sollte jeder innere Knoten eventuell zwei getrennte Listen für weitere untergeordnete innere Knoten bzw. Blatt-Knoten enthalten? Sollte er sie in einer Liste vorhalten und als Implementierungsdetail den Type-Switch für den Client-Code durchführen?

    Edit: Bliebe natürlich noch, die Typ-Auflösung per Double-Dispatch zu betreiben. Irgendwie habe ich das aber noch nicht zu schätzen gelernt, weiß auch nicht warum.



  • @Decimad: Hmm, ich bin einfach schlecht im Ausdrücken heute, aber anders gesagt: Wenn ich keine gemeinsame Basisklasse habe, kann ich im ParentNode nicht einmal eine List<ChildNode> halten die mir auch gleich autom. garantiert, dass ich nicht einen Non-ChildNode Child als einhängen kann.

    MfG SideWinder



  • SideWinder schrieb:

    @Decimad: Hmm, ich bin einfach schlecht im Ausdrücken heute, aber anders gesagt: Wenn ich keine gemeinsame Basisklasse habe, kann ich im ParentNode nicht einmal eine List<ChildNode> halten die mir auch gleich autom. garantiert, dass ich nicht einen Non-ChildNode Child als einhängen kann.

    Das Problem dabei ist aber, dass ChildNode kein gute Klasse ist. Du willst eine List<Node> haben. Ob es einen parent hat oder nicht, ist doch eigentlich irrelevant.

    Warum genau muss ein Leaf Node anders behandelt werden als ein Non-Leaf Node?

    Was den Boilerplate Code betrifft: klar ist es toll wenn man wenig Boilerplate hat - aber das ist kein Argument fuer ein Konzept. Ich verstehe schon was du meinst - aber du musst mit dieser Argumentation aufpassen, da es oft irrelevant ist wieviel Schreibarbeit man hat wenn die Modellierung stimmt. Und darum geht es: das Model muss passen. Wenn ich mit 3 Zeilen ein komplexes Modell bauen kann ist das super. Aber man sollte lieber das korrekte modell in 20 Zeilen schreiben als ein falsches Modell in 2 Zeilen.

    Es ist ein praktisches Feature einer Sprache, wenn es viele Modelle oder Ausdruecke direkt per wenigen Keywords kann - aber man darf NIE NIE NIE falsch modellieren nur weil die falsche modellierung weniger Schreibarbeit ist.



  • Nun, von müssen kann natürlich keine Rede sein, aber in meinem Fall unterscheiden sich Blattknoten von den inneren Knoten nicht alleine durch die Abwesehnheit von Kindknoten.
    Geht hier einfach um einen Baum von Liniensegmenten, bei denen der Enpunkt eines Liniensegments gleichzeitig der Startpunkt der Kind-Segmente ist, wobei das letzte Liniensegment aber nicht auffächert, sondern an etwas anderes andockt. Geht hier um graphische Repräsentation von Kanten-Bäumen in einem Netzwerk.
    So stelle ich mir das zumindest gerade alles vor.



  • Ich kann vielleicht mit einer List<Node> noch leben, und dank Interfaces habe ich zumindest in diesem Bereich (Collection-Haltung von Objekten) keine Probleme in den gängigen SI-only-Sprachen.

    Trotzdem: Element ist ein ParentNode und ist ein ChildNode. Das ist das korrekte Modell. Ein ParentNode hat eine List<Node/ChildNode> als Kinder, ein ChildNode hat einen Up-Pointer.

    Es gibt reine ParentNodes im DOM, es gibt reine ChildNodes und es gibt Node-Typen die sind nunmal beides.

    Mag sein, dass der DOM-Standrad Käse ist was OO betrifft, aber das hilft mir bei der Implementierung desselbigen auch wenig.

    Die meisten Implementierungen (bspw. Apache Xerces) leiten nun ParentNode von ChildNode ab und leben mit einem Null-Up-Pointer und anderen Kleinigkeiten die ChildNodes haben und ParentNodes nicht, aber letzten Endes ist das Modell falsch umgesetzt, und genau das bekreidest du ja ausdrücklich, nur, dass ich bspw. in Java keine Möglichkeit habe das korrekt auszudrücken - nicht einmal sher umständlich und hässlich.

    MfG SideWinder



  • Ne, das Modell ist nicht falsch umgesetzt. Die Trennung Leaf/NonLeaf gibt es nicht. Wenn ich an einen Leaf Knoten einen Knoten dranhaenge ist er nicht mehr Leaf.

    Man kann auch einfach uebermodellieren. Welche Probleme siehst du, wenn es nur eine Knotenklasse gibt die einen optionalen Parent Zeiger hat und eine optionale Liste an Kind-Knoten?

    Welche Probleme entstehen, wenn es so modelliert ist?

    Gerade bei DOM Knoten hat jeder Knoten immer potentielle Kinder und bis auf root jeder Knoten einen Parent.

    Wenn du zB den Parent-Zeiger einsparen willst, aus Platz/Performance Gruenden, dann kostet dich die vtable ploetzlich wieder mehr Platz und Performance als die Einsparung. Auch ist die Frage wie praktisch sollen die Algorithmen auf dem Baum laufen? Will ich beim traverse jedesmal dynamic casts haben um zu checken ob der Knoten Kinder hat?

    Man muss beim modellieren etwas aufpassen dass man nicht das wesentiliche aus den Augen verliert. ein String ist zB einfach ein char-array und keine abstrakte Collection bestehend aus lauter abstrakten Character-Klassen.



  • Leider kennst du dich mit den Eigenheiten von DOM nicht gut genug aus, wenn du glaubst, dass jeder Knoten potentiell Kindknoten haben kann und nur der Root-Knoten keinen Vater hat. Dann ist es natürlich jetzt schwer für dich zu modellieren. Es gibt Knoten im DOM (nein, nicht "Element", ein Element ist nur ein Typ von Knoten) die keine Kind-Knoten haben können, auch nicht potentiell.

    Ja, die DOM-API bietet trotzdem Node.getChildNodes() welches null liefert für alle non-ParentNodes bzw. nur in ParentNode überladen wird. Man könnte natürlich hier wie gesagt schon den DOM-Standard ankreiden, aber wenn man ihn implementieren will dann wohl mit dieser Vererbung und am schönsten mit MI - mal ganz abgesehen von Performance.

    MfG SideWinder


Anmelden zum Antworten