Frage zum Returnen von const Objekten



  • Nexus schrieb:

    DStefan schrieb:

    Hiermit verletzt du aber das Prinzip der Kapselung! Man kann kaum, wie im Beispiel, eine (const) Referenz zurück liefern, ohne auch eine entsprechende Instanzvariable zu verwenden.

    Ja, Referenzen müssen schliesslich etwas referenzieren, meist sind das Membervariablen. Was ist schlecht daran, eine Const-Referenz auf einen Member zurückzugeben und sich so eine möglicherweise teuere und sinnlose Kopie zu sparen? Inwiefern soll das die Kapselung verletzen?

    Bei den Non-Const-Referenzen stimme ich dir im Allgemeinen schon eher zu, wobei es einige berechtigte Ausnahmen gibt (z.B. bei Indexzugriff).

    Ja, es gibt berechtigte Ausnahmen. Einen Indexzugriff würde ich übrigens nicht darunter packen (du meinst vermutlich etwas wie std::vector?). In dieser Hinsicht habe ich mich falsch ausgedrückt - sorry.

    Was ich meinte, sind Beispiele wie das gezeigte. Ich finde, man sollte tatsächlich die "teure und sinnlose" Kopie in Kauf nehmen, es sei denn, man hätte einen guten Grund, der dagegen spricht. Die Tatsache, dass kopiert werden muss, ist in der Regel kein solcher Grund.

    Die Rückgabe von Referenzen (oder Pointern) auf Member verletzt die Kapselung, weil man bei solchen Schnittstellen kaum noch anders kann, als sie per Instanzvariable zu bedienen. Was wäre, wenn ich bei meinem Beispiel aus irgend einem Grund den Namen intern nicht mehr als std::string repräsentieren könnte oder wollte? Ich müsste nicht nur die interne Repräsentation sondern auch die Schnittstelle der Klasse ändern. Was wiederum zwangsläufig zu Änderungen in den Kunden der Klasse führt. Fazit: Die Rückgabe von Verweisen auf Membervariablen verletzt die Kapselung der Klasse.

    Oder was verstehst du unter Kapselung?

    Stefan.



  • DStefan schrieb:

    Ja, es gibt berechtigte Ausnahmen. Einen Indexzugriff würde ich übrigens nicht darunter packen (du meinst vermutlich etwas wie std::vector?).

    Weshalb nicht? Ich finde das sehr praktisch. Hier scheint mir die Kapselung auch nicht besonders wichtig. Ich hätte zumindest etwas dagegen, wenn ich statt

    vec[5] *= 2 + vec[3];
    

    das schreiben müsste:

    vec.set_at(5, vec.get_at(5) * (2 + vec.get_at(3)));
    

    Ausserdem kann man da sämtliche Funktionen, die eine Referenz oder einen Zeiger nehmen und ein Element verändern sollen, nicht mehr direkt auf die Elemente anwenden (über den Index). Mit der momentanen Vorgehensweise wird niemand daran gehindert, selbst freie Funktionen zu schreiben, die ein Element entweder setzen oder zurückgeben - umgekehrt wäre das jedoch nicht möglich. Man müsste eine hässliche Schreibweise und eventuell sogar Ineffizienz in Kauf nehmen für einen Fall, den man möglicherweise gar nicht braucht.

    DStefan schrieb:

    Was ich meinte, sind Beispiele wie das gezeigte. Ich finde, man sollte tatsächlich die "teure und sinnlose" Kopie in Kauf nehmen, es sei denn, man hätte einen guten Grund, der dagegen spricht. Die Tatsache, dass kopiert werden muss, ist in der Regel kein solcher Grund.

    Mir reicht es, keinen Grund dafür zu haben (obwohl mir Performance grundsätzlich auch wichtig ist, denke z.B. an einen std::vector statt std::string , der hält meist mehr und grössere Elemente). Nicht dass wir uns missverstehen: Ich spreche über die Rückgabe von Const-Referenzen. Dass Non-Const-Referenzen auf Membervariablen oft nicht ideal sind, ist mir bewusst.

    DStefan schrieb:

    Die Rückgabe von Referenzen (oder Pointern) auf Member verletzt die Kapselung, weil man bei solchen Schnittstellen kaum noch anders kann, als sie per Instanzvariable zu bedienen.

    Wie meinst du das genau? Per Instanzvariable bedienen? Man hat ja entsprechende Funktionen, auf die Variablen greift man nicht direkt zu.

    DStefan schrieb:

    Was wäre, wenn ich bei meinem Beispiel aus irgend einem Grund den Namen intern nicht mehr als std::string repräsentieren könnte oder wollte? Ich müsste nicht nur die interne Repräsentation sondern auch die Schnittstelle der Klasse ändern. Was wiederum zwangsläufig zu Änderungen in den Kunden der Klasse führt.

    Das ist klar. Aber den Code ändern musst du genauso, wenn du eine Kopie zurückgibst. Worauf willst du also hinaus?

    DStefan schrieb:

    Fazit: Die Rückgabe von Verweisen auf Membervariablen verletzt die Kapselung der Klasse.

    Diese Schlussfolgerung kann ich, wie schon angetönt, nicht ganz nachvollziehen. Dazu müsstest du mir erklären, warum Verweise beim Refactoring ein grösseres Problem darstellen als Kopien. Und wo der grosse Vorteil von Kopien liegt.

    DStefan schrieb:

    Oder was verstehst du unter Kapselung?

    Ich glaube, schon dasselbe wie du - also dass der Fokus auf einer möglichst kleinen Schnittstelle liegt, während Implementierungsdetails nach aussen hin verborgen bleiben. Aber dass heisst nicht, dass alles maximal gekapselt sein muss. Ich habe zum Beispiel ab und zu struct s mit komplett öffentlichem Zugriff und Konstruktoren.



  • @Nexus
    Jetzt mal abgesehen davon, dass man die const-Referenz locker in einen Lvalue umwandeln und somit ohne das Wissen des Objekts Member verändern kann, erklär doch mal, in welchen Fällen da ein Performancegewinn auftritt.



  • Mitleid schrieb:

    Jetzt mal abgesehen davon, dass man die const-Referenz locker in einen Lvalue umwandeln und somit ohne das Wissen des Objekts Member verändern kann

    Wie? Und sag nicht const_cast. Ist doch klar, daß das nicht gilt.
    Sonst könnte man auch argumentieren, daß man mit reinterpret_cast in jede Klasse kommt und dort private Daten plattmachen kann.



  • Nexus schrieb:

    DStefan schrieb:

    Ja, es gibt berechtigte Ausnahmen. Einen Indexzugriff würde ich übrigens nicht darunter packen (du meinst vermutlich etwas wie std::vector?).

    Weshalb nicht? Ich finde das sehr praktisch. Hier scheint mir die Kapselung auch nicht besonders wichtig. Ich hätte zumindest etwas dagegen, wenn ich statt

    vec[5] *= 2 + vec[3];
    

    das schreiben müsste:

    vec.set_at(5, vec.get_at(5) * (2 + vec.get_at(3)));
    

    Ausserdem kann man da sämtliche Funktionen, die eine Referenz oder einen Zeiger nehmen und ein Element verändern sollen, nicht mehr direkt auf die Elemente anwenden (über den Index). Mit der momentanen Vorgehensweise wird niemand daran gehindert, selbst freie Funktionen zu schreiben, die ein Element entweder setzen oder zurückgeben - umgekehrt wäre das jedoch nicht möglich. Man müsste eine hässliche Schreibweise und eventuell sogar Ineffizienz in Kauf nehmen für einen Fall, den man möglicherweise gar nicht braucht.

    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.

    Nexus schrieb:

    DStefan schrieb:

    Was ich meinte, sind Beispiele wie das gezeigte. Ich finde, man sollte tatsächlich die "teure und sinnlose" Kopie in Kauf nehmen, es sei denn, man hätte einen guten Grund, der dagegen spricht. Die Tatsache, dass kopiert werden muss, ist in der Regel kein solcher Grund.

    Mir reicht es, keinen Grund dafür zu haben (obwohl mir Performance grundsätzlich auch wichtig ist, denke z.B. an einen std::vector statt std::string , der hält meist mehr und grössere Elemente). Nicht dass wir uns missverstehen: Ich spreche über die Rückgabe von Const-Referenzen. Dass Non-Const-Referenzen auf Membervariablen oft nicht ideal sind, ist mir bewusst.

    Der Unterschied zwischen const und nicht-const Referenzen ist natürlich schon wichtig. Trotzdem verletzten auch const Referenzen die Kapselung und das ist schon ziemlich ernst.

    Natürlich würde ich keine Collections kopieren mögen, es sei denn, ich könnte sicher sein, dass sie relativ klein sind. Aber die Alternative ist doch nicht, Referenzen auf die Collections zurück zu geben. Ich würde Iteratoren bevorzugen oder eine andere Vorgehensweise - je nach Lage der Dinge.

    Nexus schrieb:

    DStefan schrieb:

    Die Rückgabe von Referenzen (oder Pointern) auf Member verletzt die Kapselung, weil man bei solchen Schnittstellen kaum noch anders kann, als sie per Instanzvariable zu bedienen.

    Wie meinst du das genau? Per Instanzvariable bedienen? Man hat ja entsprechende Funktionen, auf die Variablen greift man nicht direkt zu.

    Mit "bedienen" meine ich, wenn du eine Schnittstelle wie in meinem Beispiel hast, kannst du kaum anders, als eine Referenz auf eine Instanzvariable zurückzugeben.

    Stimmt, man hat entsprechende Funktionen. Aber das ist nur Augenwischerei! Die Kapselung wird (bei nicht-const Referenzen) komplett aufgehoben und bei const Referenzen ist sie nur noch hauchdünn. Wie ich schon sagte, man hat kaum noch eine Möglichkeit, die Implementierung zu ändern. 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.

    Nexus schrieb:

    DStefan schrieb:

    Was wäre, wenn ich bei meinem Beispiel aus irgend einem Grund den Namen intern nicht mehr als std::string repräsentieren könnte oder wollte? Ich müsste nicht nur die interne Repräsentation sondern auch die Schnittstelle der Klasse ändern. Was wiederum zwangsläufig zu Änderungen in den Kunden der Klasse führt.

    Das ist klar. Aber den Code ändern musst du genauso, wenn du eine Kopie zurückgibst. Worauf willst du also hinaus?

    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.

    Nexus schrieb:

    DStefan schrieb:

    Fazit: Die Rückgabe von Verweisen auf Membervariablen verletzt die Kapselung der Klasse.

    Diese Schlussfolgerung kann ich, wie schon angetönt, nicht ganz nachvollziehen. Dazu müsstest du mir erklären, warum Verweise beim Refactoring ein grösseres Problem darstellen als Kopien. Und wo der grosse Vorteil von Kopien liegt.

    Siehe oben. Es geht nicht um das Vorhandensein von Verweisen in der Schnittstelle. Sondern es geht um den Effekt von Änderungen in der Implementierung und darum, dass diese höchstwahrscheinlich auch Änderungen in der Schnittstelle und damit bei Kunden der Klasse notwendig machen. Refactoring kann auf diese Weise zum Alptraum werden.

    Nexus schrieb:

    DStefan schrieb:

    Oder was verstehst du unter Kapselung?

    Ich glaube, schon dasselbe wie du - also dass der Fokus auf einer möglichst kleinen Schnittstelle liegt, während Implementierungsdetails nach aussen hin verborgen bleiben. Aber dass heisst nicht, dass alles maximal gekapselt sein muss. Ich habe zum Beispiel ab und zu struct s mit komplett öffentlichem Zugriff und Konstruktoren.

    Naja ... an möglichst kleine Schnittstellen denke ich im Zusammenhang mit Kapselung eigentlich weniger. Es ist im Wesentlichen das Verbergen der Implementierung. 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.

    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.

    Stefan.



  • 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.

    Sowas ist mir noch nie untergekommen und wird mich auch nicht belasten. Ich gebe gerne const-Referenzen zurück.
    Wenn meine Schnittstelle andeuten will, daß das Member oder Sonstwie-Objekt weiterlebt und ohne Nachfrage von außen lesbar sein soll, gebe ich wohl einen const-Zeiger zurück. Const-Referenz ist nur Kopieroptimierung. Und natürlich darf man nicht vergessen, daß man alle Kopierkonstruktoren, die nicht ausdrücklich sinnvoll sind, verbieten sollte, was es dann gar manchmal nicht erlaubt, Kopien rauszugeben.
    Ich nehme an, daß auch Du nicht im entferntesten auf die Idee kämst, die die Referenz zu speichern. Das machen doch nur böse Leute. Ich finde nicht, daß man sich gegen böswillige Angreifer schützen muß.



  • Mitleid schrieb:

    Sebastian Pizer schrieb:

    Irgendwer ist auf diese blöde Idee gekommen, const Objekte zu "returnen", [...]

    Irgendwer? Es war, unter anderen, sein Majestät der Oberguru Scott Meyers. Camper hat dazu einen lesenswerten Beitrag verfasst, auch wenn man nicht seiner Meinung sein muss

    camper schrieb:

    Im Grunde geht das los mit der Überschrift: "Use const whenever possible" - ein etwas vorsichtigere Formulierung wäre hier schon angebracht, denn dieser Grundsatz erlaubt ja kein Abwägen.
    [...]
    Meyers empfiehlt als Rückgabetyp von Funktionen const T um die Fehlerhäufigkeit beim Nutzer ohne Einbuße für Sicherheit und Effizienz zu reduzieren. [...] Begründet wird dies mit dem Konstrukt [ a*b=c wo a*b==c hätte] stehen sollen.

    Man kann zumindest anzweifeln, dass das eine häufige Fehlerquelle ist. Gute Compiler warnen auch in solchen Fällen. Wenn man wirklich if (a*b=c) meint und die Warnung wegbekommen will, schreibt man if ((a*b=c)) stattdessen -- so ist es zumindest beim GCC. Das "ohne Effizienz zu reduzieren" stimmt nicht ganz -- spätestens seit Rvalue-Referenzen.

    Scott Meyers hat selbst gesagt (in einem anderen Kontext irgendwo in comp.std.c++ oder comp.lang.c++.moderated), dass im Zuge der C++0x Erweiterungen einige der "rules of thumb" und "design guidelines" überholt werden müssen. Ich bin mir fast sicher, dass er diese const-Design-Regel durch "Use lvalue ref qualifiers for assignment operators" ersetzten wird.

    Von C++0x abgesehen .... Ich meine mich zu erinnern, dass man auch in C++98 durch const Compileroptimierungen ausschaltet. Ich habe die copy elision Regeln nicht im Kopf, werde es aber später nochmal nachlesen.

    Ich habe großen Respekt vor Scott Meyers. Er ist aber auch nicht unfehlbar. Seine Positionen bzgl der optimalen Semantik des std::thread-Konstruktors ist zumindest fragwürdig.

    Gruß,
    SP



  • Sebastian Pizer schrieb:

    Von C++0x abgesehen .... Ich meine mich zu erinnern, dass man auch in C++98 durch const Compileroptimierungen ausschaltet. Ich habe die copy elision Regeln nicht im Kopf, werde es aber später nochmal nachlesen.

    Aus N1905.pdf, 12.8/15 (Seite 224):

    When certain criteria are met, an implementation is allowed to omit the copy construction of a class object, even if the copy constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy operations is permitted in the following circumstances (which may be combined to eliminate multiple copies):

    • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object with the same cv-unqualified type as the function return type, the copy operation can be omitted by constructing the automatic object directly into the function’s return value
    • when a temporary class object that has not been bound to a reference (12.2) would be copied to a class object with the same cv-unqualified type, the copy operation can be omitted by constructing the temporary object directly into the target of the omitted copy

    Hmmm... Wie ist das zu verstehen?

    Ich verstehe das so:

    const foo operator+(const foo& lhs, const foo& rhs) {
      foo tmp = lhs;
      tmp += rhs;
      return tmp; // NRVO nicht erlaubt, weil Qualifizierungen nicht übereinstimmen
    }
    
    int main() {
      foo a (23);
      foo b (42);
      foo c = a + b; // Keine copy elision, weil Qualifizierungen nicht übereinstimmen
    }
    

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

    Gruß,
    SP



  • volkard schrieb:

    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.

    Sowas ist mir noch nie untergekommen und wird mich auch nicht belasten. Ich gebe gerne const-Referenzen zurück.
    Wenn meine Schnittstelle andeuten will, daß das Member oder Sonstwie-Objekt weiterlebt und ohne Nachfrage von außen lesbar sein soll, gebe ich wohl einen const-Zeiger zurück. Const-Referenz ist nur Kopieroptimierung. Und natürlich darf man nicht vergessen, daß man alle Kopierkonstruktoren, die nicht ausdrücklich sinnvoll sind, verbieten sollte, was es dann gar manchmal nicht erlaubt, Kopien rauszugeben.
    Ich nehme an, daß auch Du nicht im entferntesten auf die Idee kämst, die die Referenz zu speichern. Das machen doch nur böse Leute. Ich finde nicht, daß man sich gegen böswillige Angreifer schützen muß.

    Schon Recht, das Beispiel war wohl etwas konstruiert. Es war schon spät gestern, OK?

    Ich möchte gar nicht bezweifeln, dass die Rückgabe von Verweisen auf Instanzvariablen unter Umständen sinnvoll und richtig sein kann. Meistens aber, meine ich, ist es nicht nötig. Und ich finde, die Verletzung der Kapselung sollte in der Regel Grund genug sein, solche "Kopieroptimierung" zu unterlassen.

    Anscheinend ist mir die Kapselung ein höheres Gut als die Vermeidung von Kopier-Aktionen. Anscheinend geht's dir genau anders herum. Naja...

    Stefan.



  • volkard schrieb:

    Const-Referenz ist nur Kopieroptimierung.
    ...
    Ich nehme an, daß auch Du nicht im entferntesten auf die Idee kämst, die die Referenz zu speichern.

    OK, obwohl ich noch einen Qualitätsunterschied zwischen einem normalen const_cast und einem reinterpret_cast-Hack mit möglichem UB sehe, sei das Argument geschenkt. Aber, wie DStefan schon aufführt ist auch der rein lesende Zugriff ohne Verwendung der Schnittstelle bereits eine Verletzung des Geheimnisprinzips mit den entsprechenden Problemen.

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



  • 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