Frage zum Returnen von const Objekten



  • Sebastian Pizer schrieb:

    Trotzdem werden diese Optimierungen bei G++ 3.4.5 durchgeführt.

    Wie hast du das getestet? Hast du es mal außerhalb einer Initialisierung versucht?



  • DStefan schrieb:

    std::vector ist ein anderer Fall. Du erhältst hier ja Referenzen auf die Objekte im Vector, nicht auf Instanzvariablen der Klasse. Die Kapselung wird durch diese Schnittstelle nicht verletzt. Oder zumindest nicht mehr als unbedingt notwendig.

    Okay, dann habe ich dich hier falsch verstanden.

    DStefan schrieb:

    Ein Kunde könnte die Referenz speichern und dann immer wieder direkt auf die Variablen zugreifen, ohne dass die Funktion noch einmal aufgerufen werden muss, und so weiter.

    Ja, aber das sollte man wirklich nicht tun, zumindest nicht über lange Zeit hinweg. Allein schon weil die Möglichkeit besteht, dass die Referenz invalidiert wird. Wenn man den Wert der Klasse braucht und speichern will, kann man ja ohne weiteres immer noch eine Kopie anlegen.

    DStefan schrieb:

    Komm schon! Das ist doch wohl nicht dein Ernst. Ich müsste nicht bloß den Code der betreffenden Klasse, sondern auch den Code aller Kunden der Klasse ändern. Ich müsste alle Stellen finden, in denen die Methode verwendet wird und dort Änderungen durchführen. Möglicherweise mit erheblichem Aufwand.

    Doch, das ist mein Ernst. Vielleicht übersehe ich gerade etwas Wichtiges, in diesem Fall tut es mir leid.

    Aber um dein Beispiel mit dem String anzubringen:

    // Klasse
    class MyClass
    {
        public:
            // Rückgabe einer Kopie, wie von dir vorgeschlagen
            std::string GetName() const;
    };
    
    // Benutzung
    void Bla(const std::string& s);
    
    void Foo()
    {
        MyClass b;
        std::string Name = b.GetName();
        Bla(Name);
    }
    

    Wenn GetName() jetzt keinen std::string , sondern einen X77String zurückgibt, darfst du genauso alles anpassen!

    DStefan schrieb:

    Indem du aber Verweise auf Instanzvariablen in der (öffentlichen) Schnittstelle lieferst, durchbrichst du die Kapselung und verlierst damit einige wesentliche Vorteile dieser Technik. Du verlierst Implementierungsfreiheit, der Code wird schwerer änderbar.

    Bei Non-Const-Referenzen, ja. Bei Const-Referenzen wird der Code normalerweise genauso schwer änderbar wie mit Kopien. Es sei denn, du bringst endlich mal ein Gegenbeispiel.

    DStefan schrieb:

    Klar, ich verwende ebenfalls gelegentlich structs genauso wie du. Aber (wie du vermutlich auch) ich überlege mir sehr gut, wann ich das mache. Weil ich (oder einer meiner Kollegen) möglicherweise teuer dafür bezahlen muss, wenn's einmal an's Refactoring geht.

    Ja, aber du solltest Kapselung nicht über alles erheben. Oft bringt diese nämlich keinen wesentlichen Vorteil. Wenn du in einem kleinen struct etwas refactoren musst, werden meistens neue Member hinzugefügt/entfernt. Da gewinnst du keinen Vorteil über Getter und Setter, da du diese in gleichem Masse hinzufügen/entfernen musst und den Anwendungscode ebenso anpassen kannst (im Falle der Entfernung).

    Mitleid schrieb:

    Und wenn die Referenz nicht gespeichert wird, wo war dann nochmal die Kopieroptimierung?

    Selbst wenn die Funktion nicht geinlinet wird, ist ein Rückgeben einer Const-Referenz immer noch etliche Male schneller als zum Beispiel die Kopie eines ganzen Containers.



  • Nexus schrieb:

    Selbst wenn die Funktion nicht geinlinet wird, ist ein Rückgeben einer Const-Referenz immer noch etliche Male schneller als zum Beispiel die Kopie eines ganzen Containers.

    Das bezweifelt ja niemand, nur würde ich gerne ein paar Anwendungsfälle sehen in welchen das konkret etwas bringt.

    Als Grundlage könnten wir ja DStefans Klasse nehmen:

    class Person {
    public:
    
    //   std::string &name() {
    //      return _name;
    //   }
    
       const std::string &name() const {
          return _name;
       }
    
    private:
       std::string _name;
    };
    

    So, wie verwendest du jetzt name() optimal?



  • Mitleid schrieb:

    Sebastian Pizer schrieb:

    Trotzdem werden diese Optimierungen bei G++ 3.4.5 durchgeführt.

    Wie hast du das getestet? Hast du es mal außerhalb einer Initialisierung versucht?

    Damit:

    #include <iostream>
    
    using std::cout;
    
    class foo
    {
    public:
      foo(){}
      foo(foo const& x) {cout<<"copy-ctor\n";}
      foo& operator=(foo const& x) {cout<<"op=\n";return *this;}
      foo& operator+=(foo const& x) {cout<<"op+=\n";return *this;}
    };
    
    foo operator+(foo const& x, foo const& y)
    {
      foo t = x; // Kopie!
      t += y;
      return t;  // Kopie für Ergebnis ?
    }
    
    int main()
    {
      foo a;
      foo b;
      foo c = a + b; // Temp-Obj nach c kopieren ?
    }
    

    Egal, ob operator+ foo oder const foo zurückgibt, die Ausgabe ist bei G++ 3.4.5 immer die folgende:

    copy-ctor
    op+=
    

    G++ führt also beide "copy elisions" in beiden Fällen durch. Also, entweder habe ich den Standard falsch verstanden (durchaus möglich) oder G++ hält sich nicht an die zitierten Regeln.

    Gruß,
    SP



  • Nexus schrieb:

    Aber um dein Beispiel mit dem String anzubringen:

    // Klasse
    class MyClass
    {
        public:
            // Rückgabe einer Kopie, wie von dir vorgeschlagen
            std::string GetName() const;
    };
    
    // Benutzung
    void Bla(const std::string& s);
    
    void Foo()
    {
        MyClass b;
        std::string Name = b.GetName();
        Bla(Name);
    }
    

    Wenn GetName() jetzt keinen std::string , sondern einen X77String zurückgibt, darfst du genauso alles anpassen!

    Ok, es ist wohl wirklich ein Beispiel nötig. Der Einfachheit halber alles inline.

    Erst wie man's (meiner Meinung nach) machen sollte

    class Person {
    
    public:
       std::string name() const {
          return _name;
       };
    
    private:
       std::string _name;
    };
    

    Aus irgend einem Grund muss die Implementierung von Person::_name geändert werden. Die Schnittstelle von Person aber kann gleich bleiben:

    class Person {
    
    public:
       std::string name() const {
          return _name.asStdString();
       };
    
    private:
       OptimizedString _name;
    };
    

    Hier bleibt die public Schnittstelle unverändert, Kunden müssen nicht angepasst sondern nur neu übersetzt werden. Keine große Sache.

    Nun das Beispiel mit einer Verletzung der Kapselung:

    class Person {
    
    public:
       const std::string &name() const {
          return _name;
       };
    
    private:
       std::string _name;
    };
    

    Und erneut die Änderung der internen Repräsentation:

    class Person {
    
    public:
       const OptimizedString &name() const {
          return _name;
       };
    
    private:
       OptimizedString _name;
    };
    

    Nun wird also die public Schnittstelle geändert. Sie muss geändert werden, da die Implementierung von Person::_name nicht gekapselt ist. Also müssen alle Kunden geändert werden. Der Aufwand kann unter Umständen enorm sein.

    Und falls, was natürlich bei Strings nicht allzu wahrscheinlich ist, OptimizedString eine Schnittstelle bietet, die (teilweise) mit std::string syntaktisch aber nicht semantisch gleich ist, hilft dir bei diesem Änderungen nicht einmal der Compiler. Jedenfalls nicht ohne Weiteres.

    Ich finde das nicht vertretbar!

    Nexus schrieb:

    Selbst wenn die Funktion nicht geinlinet wird, ist ein Rückgeben einer Const-Referenz immer noch etliche Male schneller als zum Beispiel die Kopie eines ganzen Containers.

    Erstens kommt es auf die Geschwindigkeit nicht an, jedenfalls nicht ohne Weiteres. Selbst wenn die Kopie des Objekts tausendmal langsamer als die Rückgabe einer Referenz ist, besteht kein Optimierungsbedarf. Es sei denn natürlich, die Optimierung kostet nichts.

    Zweitens wird man einen Container nicht kopieren, es sei denn er ist sehr klein. Man wird aber auch keine Referenz auf die Instanzvariablen zurückgeben. Vielmehr wird man eine Schnittstelle aufbauen, die die relevanten Werte liefert, ohne die Kapselung zu verletzen.

    Stefan.



  • Sebastian Pizer schrieb:

    G++ führt also beide "copy elisions" in beiden Fällen durch.

    "...the same cv-unqualified type..."

    Ich glaube du verstehst denn Satz so, als ob der cv-qualified type derselbe sein müsste. Muss er aber nicht.



  • Ah, okay, hier hast du Recht. Ich habe dich bisher immer so verstanden, dass du auch den Rückgabetypen ändern würdest. Deshalb meine Verwirrung.

    Das, was du sagst, habe ich selber auch schon oft gemacht. Zwar nicht mit Strings, aber unter anderem mit mathematischen Vektorklassen. Allerdings war der Hintergrund nicht ganz der Gleiche. Manchmal existiert der Vektor als Member, manchmal muss er berechnet werden, daher fällt eine Const-Referenz weg. Um das Interface einheitlich zu halten, wird immer eine Kopie zurückgegeben. Die ist hier aber auch sehr billig, bedenkt man, dass so ein Vektor nur wenige Bytes gross ist.

    Bei grösseren Typen hingegen würde ich mir das Ganze mehrmals überlegen. Zuerst einmal, ob eine Änderung überhaupt jemals in Frage käme (bei mir ist das bei std::string momentan zum Beispiel nicht der Fall). Und ob es sich lohnt, aus Abwärtskompatibilität die Schnittstelle um jeden Preis zu bewahren und dafür bei jedem Getter-Aufruf eine Konstruktion zu haben. Zudem, ob eine Kopie von der Performance her okay ist. Du sagst, auf die Geschwindigkeit käme es nicht an. Ich programmiere ab und zu Spiele, und da muss ich schon etwas schauen. Natürlich fallen einzelne Rückgaben nicht ins Gewicht, aber an zentralen Stellen kann man dadurch zum Teil ziemlich optimieren (jetzt nicht nur mit Strings). Grössere Typen sind aber auch oft noch einmal zusätzlich gekapselt, meistens fassen diese nämlich mehrere Eigenschaften zusammen. Oft kann man durch die Indirektion nur etwas am Typen selbst ändern, und die Schnittstelle kann auch mit Const-Referenz bewahrt bleiben. Ausserdem gibt es Fälle, wo eine Kopie gar nicht möglich ist.

    Von daher finde ich es nicht gut, Const-Referenzen als Kapselungsfehler zu pauschalisieren. Klar, wenn Dinge wie Performance nicht entscheidend sind und die Wahrscheinlichkeit hoch ist, dass sich die relevanten Interna ändern, ist eine Kopie wohl einfacher. Aber das jetzt grundsätzlich immer zu machen, weil sich etwas ändern könnte, halte ich für falsch.



  • Nexus schrieb:

    Ah, okay, hier hast du Recht. Ich habe dich bisher immer so verstanden, dass du auch den Rückgabetypen ändern würdest. Deshalb meine Verwirrung.

    Das, was du sagst, habe ich selber auch schon oft gemacht. Zwar nicht mit Strings, aber unter anderem mit mathematischen Vektorklassen. Allerdings war der Hintergrund nicht ganz der Gleiche. Manchmal existiert der Vektor als Member, manchmal muss er berechnet werden, daher fällt eine Const-Referenz weg. Um das Interface einheitlich zu halten, wird immer eine Kopie zurückgegeben. Die ist hier aber auch sehr billig, bedenkt man, dass so ein Vektor nur wenige Bytes gross ist.

    Bei grösseren Typen hingegen würde ich mir das Ganze mehrmals überlegen. Zuerst einmal, ob eine Änderung überhaupt jemals in Frage käme (bei mir ist das bei std::string momentan zum Beispiel nicht der Fall). Und ob es sich lohnt, aus Abwärtskompatibilität die Schnittstelle um jeden Preis zu bewahren und dafür bei jedem Getter-Aufruf eine Konstruktion zu haben. Zudem, ob eine Kopie von der Performance her okay ist. Du sagst, auf die Geschwindigkeit käme es nicht an. Ich programmiere ab und zu Spiele, und da muss ich schon etwas schauen. Natürlich fallen einzelne Rückgaben nicht ins Gewicht, aber an zentralen Stellen kann man dadurch zum Teil ziemlich optimieren (jetzt nicht nur mit Strings). Grössere Typen sind aber auch oft noch einmal zusätzlich gekapselt, meistens fassen diese nämlich mehrere Eigenschaften zusammen. Oft kann man durch die Indirektion nur etwas am Typen selbst ändern, und die Schnittstelle kann auch mit Const-Referenz bewahrt bleiben. Ausserdem gibt es Fälle, wo eine Kopie gar nicht möglich ist.

    Von daher finde ich es nicht gut, Const-Referenzen als Kapselungsfehler zu pauschalisieren. Klar, wenn Dinge wie Performance nicht entscheidend sind und die Wahrscheinlichkeit hoch ist, dass sich die relevanten Interna ändern, ist eine Kopie wohl einfacher. Aber das jetzt grundsätzlich immer zu machen, weil sich etwas ändern könnte, halte ich für falsch.

    Naja - ich habe doch gar nicht pauschalisiert. Jedenfalls nicht sehr 😉

    Ich glaube, meine Denkrichtung ist einfach anders als deine. So lange ich nicht weiß, dass es ein Performance-Problem gibt, möchte ich die Kapselung erhalten. Bei dir, vermute ich, ist es eher so, dass du wenig Wert auf Kapselung legst, so lange du nicht weißt (oder vermutest), dass eine Änderung der Instanzvariablen auftreten könnte.

    Das kann durchaus auch daher kommen, mit welchem Zeugs man bisher zu tun hatte. Ich habe mit Spieleprogrammierung (wo es wohl sehr auf Performanz ankommt) nichts im Sinn, sondern arbeite hauptsächlich an komplexen, lang laufenden Projekten, die teilweise drastischen Veränderungen unterworfen sind. Da ist Wartbarkeit das A und O und Kapselung ein wichtiger Faktor, während die Performance des Systems in der Regel durch Netzwerk und/oder Datenbank dominiert wird.

    Stefan.



  • Hm ja. Wahrscheinlich würde ich an deiner Stelle sogar gleich handeln. Es kommt halt doch recht auf die Anwendung drauf an, wie man programmiert. Ich kapsele sonst auch nicht gerade wenig, aber solche Generalisierungen wie bei std::string sind bei mir oft unnötig. Ich bin eigentlich auch kein spezieller Premature-Optimization-Freak, sondern versuche grundsätzlich, so gut es geht auf Wiederverwertbarkeit und sauberen Code zu achten.

    Aber gut, dass wir uns einig sind. 🙂



  • Mitleid schrieb:

    Sebastian Pizer schrieb:

    G++ führt also beide "copy elisions" in beiden Fällen durch.

    "...the same cv-unqualified type..."
    Ich glaube du verstehst denn Satz so, als ob der cv-qualified type derselbe sein müsste. Muss er aber nicht.

    Du hast Recht. Ich habe "cv-unqualified" falsch verstanden.

    Trotzdem halte ich es immernoch für sinvoller, sich auf Warnungen des Compilers zu verlassen und damit die Tür für "Move-Semantik" nicht zu verschließen.

    Gruß,
    SP


Anmelden zum Antworten