Frage zum Returnen von const Objekten
-
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 keinenstd::string, sondern einenX77Stringzurü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
structetwas 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 keinenstd::string, sondern einenX77Stringzurü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::stringmomentan 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::stringmomentan 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::stringsind 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