Operator Overloading - Effizient oder Nicht?



  • Hallo,

    Ich schreibe gerade eine Klasse fuer Brueche, um das Operator Overloading nen bisschen zu üben, nun mach ich folgendes:

    bool operator>(const Bruch& otherBruch) { 
                       int this_value = this->GetZaehler()/this->GetNenner();
                       int other_value = otherBruch.GetZaehler/otherBruch.GetNenner();
                       if (this_value > other_value) {
                         return true;
                       }
                       else {
                         return false;
                       }
                 }
                 bool operator<(const Bruch& otherBruch) { 
                       int this_value = this->GetZaehler()/this->GetNenner();
                       int other_value = otherBruch.GetZaehler/otherBruch.GetNenner();
                       if (this_value < other_value) {
                         return true;
                       }
                       else {
                         return false;
                       }
                 }
                 bool operator==(const Bruch& otherBruch) { 
                       int this_value = this->GetZaehler()/this->GetNenner();
                       int other_value = otherBruch.GetZaehler/otherBruch.GetNenner();
                       if (this_value == other_value) {
                         return true;
                       }
                       else {
                         return false;
                       }
                 }
                 bool operator!=(const Bruch& otherBruch) { 
                       int this_value = this->GetZaehler()/this->GetNenner();
                       int other_value = otherBruch.GetZaehler/otherBruch.GetNenner();
                       if (this_value != other_value) {
                         return true;
                       }
                       else {
                         return false;
                       }
                 }
    

    Ich schreib ja fast viermal das genau gleiche, is das effizient??
    und für >= und <= müsste ich nochmals zweimal das ganze schreiben...
    gibts denn keinen anderen, besseren Weg?



  • So zum Beispiel:

    bool operator<(const Bruch &otherBruch)
    {
        return GetZaehler() / GetNenner() < otherBruch.GetZaehler() / otherBruch.GetNenner();
    }
    
    bool operator>(const Bruch &otherBruch)
    {
        return GetZaehler() / GetNenner() > otherBruch.GetZaehler() / otherBruch.GetNenner(); // Edit: Da !(this < otherBruch) ja ein größer gleich und kein größer als wäre
    }
    

    (Wenn ich mich nicht irre, da ungetestet.) 😉



  • @Mizars Vorschlag (ok, auch beim TO ist das schon falsch):

    Bruch a(3, 2); //1.5000
    Bruch b(5, 3); //1.6667
    
    bool less(a<b);
    
    assert(less); //BÄM - da less immer false sein wird
    

    du wirst dir also nen anderen weg einfallen lassen, den op< zu definieren...
    aber es ist richtig, dass man op>, op>= und op<= über op< definieren kann(so gar op== und damit auch op!= - hier weiß ich aber nicht, ob der compiler in der Regel wirklich gut genug optimiert).

    op< (a, b)
    {
    //...
    }
    
    op>= (a, b)
    {
      return !op< (a, b);
    }
    
    op> (a, b)
    {
      return op< (b, a);
    }
    
    op<= (a, b)
    {
      return !op> (a, b);
    }
    
    op== (a, b)
    {
      return !(op< (a, b)) && !(op> (a, b));
    }
    
    op!= (a, b)
    {
      return !op== (a, b);
    }
    

    ein wenig unübersichtlich - bedingt durch meine Faulheit - hab extra nur code-tags genommen, damit du siehst, dass es keinesfalls sytaktisch korrekt ist und nur die idee verdeutlichen soll ;o)
    für op< brauchst du trotzdem noch ne idee 😛

    bb

    edit: oh - du hast die operatoren ja noch nicht mal als freie fkt. implementiert sondern als member Oo
    lies dir mal den artikel hier durch: (zumindest Teil 1 - mehr kann aber nicht schaden)
    http://www.c-plusplus.net/forum/viewtopic-var-t-is-232010.html



  • Wie ich sehe, bist Du, EffizientOrNot, ein Freund von nicht-statischen Elementfunktionen. In diesem Fall sind allerdings freie Funktionen eleganter:

    class Bruch
    {
      long z, n;      // Invariante: n>=1 && ggt(z,n)==1
      void kuerzen(); // stellt Invariante her
    public:
      Bruch(long z=0) : z(z), n(1) {}
      Bruch(long z, long n) : z(z), n(n) {kuerzen();} 
    
      long zaehler()  const {return z;}
      long nenner()   const {return n;}
    
      long intDivision()   const {return z/n;}
      long divisionsRest() const {return z%n;}
    };
    
    bool operator<(Bruch const b1, Bruch const b2)
    {
      long const d1 = b1.intDivision();
      long const d2 = b2.intDivision();
      if (d1<d2) return true;
      if (d1>d2) return false;
      return b1.divisionsRest()*b2.nenner()
           < b2.divisionsRest()*b1.nenner();
    }
    

    Jetzt sind folgende Aufrufe legal:

    void test(Bruch a, Bruch b)
    {
      a < b;   // #1
      a < 23;  // #2
      23 < b;  // #3
    }
    

    In Fall #2 und #3 wird die Ganzzahl 23 zu einem Bruch dank des impliziten Konstruktors konvertiert und dann operator<(Bruch,Bruch) aufgerufen. Wenn Du operator< allerdings als nicht-statische Elementfunktion implementierst, wird #3 nicht funktionieren.

    Gruß,
    SP



  • EffizientOrNot? schrieb:

    gibts denn keinen anderen, besseren Weg?

    1) Als erstes würde ich die Operatoren global machen. Es besteht kein Grund für Memberfunktionen, über GetZaehler() und GetNenner() auch von aussen auf die Attribute zugreifen kannst. Globale Funktionen erhöhen die Kapselung (weniger Funktionen haben direkten Zugriff auf private Member), im Falle der Operatoren kann eine implizite Konvertierung des ersten Elements stattfinden. Wenn du zudem plötzlich eine Überladung definierst, sodass operator< z.B. mit int symmetrisch funktioniert, musst du zwangsläufig zu globalen Funktionen greifen. Siehe dazu auch Operatorüberladung in C++.

    2) In deinem Beispiel kann man unnötige Codeduplizierung ausserdem vermeiden, indem man bedenkt, wie relationale Operatoren durch einander ausgedrückt werden können. Es reicht, wenn du operator< und operator== vollständig implementierst.

    bool operator> (const Bruch& lhs, const Bruch& rhs)
    {
        return rhs < lhs;
    }
    
    bool operator<= (const Bruch& lhs, const Bruch& rhs)
    {
        return !(rhs < lhs);
    }
    
    bool operator>= (const Bruch& lhs, const Bruch& rhs)
    {
        return !(lhs < rhs);
    }
    
    bool operator!= (const Bruch& lhs, const Bruch& rhs)
    {
        return !(lhs == rhs);
    }
    

    3) Unter der Annahme, dass GetZaehler() und GetNenner() integrale Werte zurückgeben, sind deine Vergleichsfunktionen schlecht implementiert, da durch die Integer-Divisionen Informationen verloren gehen. Bedenke, dass bei Integer-Typen beispielsweise die Identität 5/2 == 4/2 zu true evaluiert. Du müsstest also mindestens einen der Divisionsoperanden vor der Operation casten.

    4) Performancetechnisch betrachtet wäre es wahrscheinlich schneller, du würdest die Division in eine Multiplikation umwandeln (Gleichung a/b > c/d zu a*d > c*b umformen, Vorzeichen beachten). Damit bräuchtest du auch keine Fliesskommatypen. Allerdings besteht die Gefahr eines Overflows.



  • Danke Vielmal für die vielen hilfreichen Informationen!



  • Es reicht, wenn du operator< und operator== vollständig implementierst.

    Das kann keiner wissen. Manchmal hat man keine so sauberen Ordnungen, zB in IEEE 754. Da ist x < NaN falsch, aber auch NaN <= x . In solchen Fällen muss man mindestens operator< und operator== und operator<= festlegen.
    🙂



  • Habe mal den Artikel durchgeblättert und weiss nun weshalb ich freie Funktionen verwenden sollte.

    Doch wie kann ich eine freie Funktion machen, die entweder zwei Objekte Bruch oder ein Bruch und Int nimmt??



  • EffizientOrNot? schrieb:

    Doch wie kann ich eine freie Funktion machen, die entweder zwei Objekte Bruch oder ein Bruch und Int nimmt??

    Indem du sie einfach so definierst? Da wird dann schon die passende Funktion aufgerufen. Dazu solltest du aber vlt. noch den Konstruktor von Bruch explicit machen.



  • Das bedeutet ich schreibe für jeden Operator zwei Funktionen?

    Nämlich eine für zwei Brueche und eine für Bruch und ein Template für int, float, etc.?



  • µngbd schrieb:

    Es reicht, wenn du operator< und operator== vollständig implementierst.

    Das kann keiner wissen. Manchmal hat man keine so sauberen Ordnungen, zB in IEEE 754. Da ist x < NaN falsch, aber auch NaN <= x . In solchen Fällen muss man mindestens operator< und operator== und operator<= festlegen.
    🙂

    nexus' punkt fing ja nun auch an mit "in deinem beispiel"... das das nicht immer der fall ist, sollte klar sein...
    dein vergleich is btw doof gewählt - lässt sich immernoch mit nem early return lösen... 😛

    @drakon:
    hier würde ich den ctor aber nicht explicit machen - oder hast du irgend nen argument für die nicht implizite typumwandlung?

    @EffizientOrNot?:
    "Nämlich eine für zwei Brueche und eine für Bruch und ein Template für int, float, etc.?"
    nö... du bietest entsprechende nicht-explizite konstruktoren an und brauchst dann nur noch einen einzigen op< zu implementieren(nämlich einen, der 2 brüche vergleicht)...

    bb



  • @drakon:
    hier würde ich den ctor aber nicht explicit machen - oder hast du irgend nen argument für die nicht implizite typumwandlung?

    Ich dachte er wollte das eben genau nicht, sondern eben verschiedene Funktionen. Aber dem fall will er das ja doch nicht, dann können wir den ctor ohne explicit lassen.

    @EffizientOrNot:
    Wenn du den ctor so lässt, dann wird die Umwandlung eines integers in einen Bruch automatisch gemacht und du musst nix spezielles machen. Ein Bruch mit floats/doubles drin macht imo überhaupt keinen Sinn (denk da mal drüber nach). 😉



  • Das ist ja total cool.

    Heisst das, sobald ich ein Integer zu meiner Operator-Funktion schicke, obwohl dort ein Bruch erwartet wird, wird dieser Integer als Argument an meine Bruch Klasse geschickt?

    Oder wieso funktioniert das x_X?



  • Ja, weil du im Konstruktor von Bruch ja sagst, dass ein integer in einen Bruch "umgewandelt" werden kann. Das macht ja auch schon rein mathematisch einen Sinn, weil du ja überall, wo ein Bruch erwartet wird einfach die Zahl/1 übergeben kannst. Ist ja ein völlig korrekter Bruch und entspricht auch immer noch der gleichen Zahl. Das schöne ist auch, dass du jetzt zuerst einen integer und dann einen Bruch schreiben kannst (was nicht möglich wäre, wenn die Funktion nicht frei implementier worden wäre).

    Das sollte aber alles im verlinkten Artikel über Überladung genannt werden.



  • EffizientOrNot? schrieb:

    Das ist ja total cool.

    Heisst das, sobald ich ein Integer zu meiner Operator-Funktion schicke, obwohl dort ein Bruch erwartet wird, wird dieser Integer als Argument an meine Bruch Klasse geschickt?

    Hast Du meine Antwort eigentlich gelesen?

    Gruß,
    SP



  • Sebastian Pizer schrieb:

    EffizientOrNot? schrieb:

    Das ist ja total cool.

    Heisst das, sobald ich ein Integer zu meiner Operator-Funktion schicke, obwohl dort ein Bruch erwartet wird, wird dieser Integer als Argument an meine Bruch Klasse geschickt?

    Hast Du meine Antwort eigentlich gelesen?

    Gruß,
    SP

    Natürlich! Es hat mir nun gleich das Aha-Erlebnis gegeben 😃 (Mit dem Post von drakor D:)

    Vielen Dank an Alle, habt mir echt geholfen 😨

    Bye Bye 👍


Anmelden zum Antworten