Objekt in Funktion Erzeugen und selbiges zurückgeben



  • Mein Problem ist eigentlich ganz simpel. In einer Funktion möchte ich ein Objekt erzeugen, füttern und anschließend zurückgeben.
    Dabei soll es bei der Rückgabe nicht vernichtet werden. Sprich eine Kopie als Rückgabewert kommt aus Performancegründen nicht in Frage.

    Es liegt also nahe eine Referenz zurückzugeben, allerdings kommt man hier nicht weiter, da das Objekt nur lokal ist.
    Ich bin auf static gestoßen, allerdings habe ich hier auch nicht die passende Lösung gefunden. So, wie ich es verstanden habe, bleibt ein static objekt dann im speicher, ohne, dass es gelöscht wird. Zudem wäre das Objekt dann einmalig. Man könnte sich nicht mehrere Objekte über diese Funktion erzeugen lassen.
    Zeiger will ich auch nicht verwenden, weil ich den Speicher danach nicht extra freigeben will.

    Ganz konkret möchte ich den + Operator überladen. Dabei soll ein neues Objekt erzeugt und zurückgeben werden.
    Zum Üben habe ich eine Klasse MyDate geschrieben. Darin soll folgende Funktion vorkommen:

    class MyDate {
    public:
    	//...
    	MyDate operator +(int rhs);
    	//...
    };
    

    Wenn ich also ein MyDate-Objekt mit einem Integer addiere, dann soll ein MyDate-Objekt (oder auch eine Referenz) zurückgegeben werden, dass r Tage entfernt liegt.

    Wie gesagt möchte ich das in der Funktion erzeugte Objekt behalten und nicht kopieren. Es soll dann aber wie ein normales Objekt sterben, nur ein bisschen später:

    {
    	MyDate lhs = MyDate(1, 2, 2000) 	// 1.2.2000
    	int rhs = 4;
    	MyDate date = lhs + rhs; 		// Erzeugung von 5.2.2000
    	//...
    } //hier sollte das objekt, das in lhs.operator+(rhs) erzeugt wurde, sterben
    

    Im Prinzip müsste + sich dann wie ein Constructor verhalten.

    Ich bitte um elegante Lösungen
    Ich bedanke mich schon mal 😉



  • IMHO siehst Du hier Optimierungsbedarf wo (noch) keiner besteht. Deshalb wäre in meinen Augen die eleganteste Lösung eine der offensichtlichsten: Die operator+-Methode wird so implementiert, dass ein unnamed temporary zurückgegeben wird (oft ideal: return MyDate(*this) += arg) und der Compiler kann die Kopie eliminieren, wenn er die RVO durchführt.



  • Funktionen, die Objekte erzeugen können sie auf 3 Weisen zurückgeben:

    1. Per Referenz auf ein static member. Wie du schon bemerkt hast, ist das zurückgegebene Objekt dann einmalig.
    2. Per Zeiger. Es muss dann allerdings irgendwie dafuer gesorgt werden, dass das Objekt später wieder zerstört wird.
    3. Per value. Zumindets in der Theorie beinhaltet das wenigstens einen Aufruf des Copy-Ctors

    Anders gehts nicht. ABER:
    Zu 2) Wenn die Funktion einen auto_ptr zurueckgibt, entfällt das delete, da dies vom auto_ptr selbst übernommen wird. Allerdings ist das gerade bei operator+ eher unüblich um nicht zu sagen unintuitiv und verwirrend.

    Zu 3) Compiler können bei Rückgabewerten von Funktionen die Copy-Ctors wegoptimieren. Dabei ist wichtig, wie du deinen operato+ implementierst. Wenn das neu konstruierte Objekt in deiner Funktion ein temporäres Objekt ist, stehen die Chancen gut, dass der Compiler die Kopie vom lokalen Objekt ins Rückgabeobjekt eliminiert genauso wie die Kopie des Rückgabewerts an deine Variable, der du den Funktionswert zuweist. Ein wichtiger Schritt dazu ist, einen Operator+ immer durch den operator+= zu implementieren

    bsp:

    MyDate& MyDate::operator+=(int i) 
    {
      mDay += i;
      return *this;
    }
    
    MyDate MyDate::operator+(int i) //schlechtere version
    {
      MyDate result(*this);  //1. Konstruktor
      result += i;
      return result; //2. Konsturktor, result wird an den Rückgabewert kopiert
    }
    
    MyDate MyDate::operator+(int i) //bessere version
    {
      return MyDate(*this) += i; //Eine Konstruktion ist immer, aber die Kopie an den Rückgabewert kann eliminiert werden weils eine temporäre Variable ist.
    }
    

    Genauere Details zu dem Thema findest du auch in Meyers' (More) Effective C++


  • Mod

    LordJaxom schrieb:

    (oft ideal: return MyDate(*this) += arg) und der Compiler kann die Kopie eliminieren, wenn er die RVO durchführt.

    Das ist gerade kein Fall, auf den RVO angewandt werden kann, denn des Argument von return ist kein temporäres Objekt sondern der Rückgabewert des überladenen += Operators, ein lvalue.
    Das ist bei der Implementation von + durch += u.a. immer so (ich hab vor kurzem einen etwas längeren Text geschrieben, warum das ohnehin keineswegs immer so optimal ist), NRVO kann man in der Regel einsetzen. Natürlich kann das Ergebnis auch bei diesem Konstrukt gleich ausfallen, aber dann müssen wir uns auf die as-if-Regel verlassen, die wirklich keinerlei Garantien hinsichtlich Optimierung bietet.



  • Tatsache, Du hast (wie immer) recht. Leider ist mein EffC++ gerade ausser Haus und der MoreEffC++ ist noch eine Auflage aus prä-NRVO Zeiten, aber selbst in diesem alten Buch endet Meyers mit der benannten Variante, bei der die Möglichkeit der Optimierung zumindest besteht (mit der Fußnote, dass NRVO inzwischen möglich ist).

    Damit hat sich pumuckls schlechtere Variante gerade zur besseren gemausert 😃



  • @LordJaxom

    da habe ich ein paar fragen
    wie soll der ausdruck

    MyDate(*this) += arg
    

    umgesetzt werden?

    der += operator ist nicht definiert

    außerdem wird mit MyDate(*this) wieder ein Objekt erzeugt, dass beim Rückgeben gleich wieder zerstört wird.
    das objekt, dass tatsächlich zurückgegeben wird, ist nur eine kopie
    gerade dass wollte ich nicht.

    bei einem MyDate-Objekt ist das kopieren ja noch lange kein Performanceproblem

    allerdings können andere Objekte später sehr viel größer werden. Diese dann zu kopieren, kann dann echt ziemlich belastend werden.



  • Deshalb erwähnte ich ja die RVO (ich hoffte Du suchst wenigstens mal danach, das heisst "Return Value Optimization"). Die hätte es möglich gemacht, die erwähnte Kopie zu eliminieren, was aber wie wir gerade gelernt haben nicht klappt. Damit bleibt noch die named Variante, da kannst Du evtl. einfach mal schauen was Dein Compiler daraus macht.

    Zum operator+=, wenn der nicht da ist musst Du ihn natürlich vorher implementieren, da sollte eigentlich nichts gegen sprechen. Den operator+ auf Basis des operator+= zu implementieren ist eigentlich nur folgerichtig, da der Operator + auf niedrigstem Niveau sowieso als Kopie von links += rechts abgebildet wird (es seidenn man hat eine Dreiadressmaschine 😉 )



  • @LordJaxom

    sicherlich habe ich mal kurz unter rvo gegooglet, habe deine variante allerdings erstmal versucht auszuprobieren, bis ich feststellte, dass trotzdem ein neues objekt erzeugt und gleich darauf wieder zerstört wurde

    lieg ich da richtig, dass die rvo in speziellen fällen dafür sorgt, dass statt einer kopie eines temporären objektes, dass temporäre objekt selber übergeben wird?

    ich habe folgendes ausprobiert:

    #include<iostream>
    
    using namespace std;
    
    class X {
    public:
    	int n;
    	X(int n) {
    		this->n = n;
    	}
    	~X() {
    		cout << "vernichte " << this << endl;
    	}
    	X operator +=(int rhs) {
    		n += rhs;
    		return *this;
    	}
    	X operator +(int rhs) {
    		return X(*this) += rhs;
    	}
    };
    
    int main()
    {
    	X x = X(2);
    	X z = x + 3;
    	cin.get();
    }
    

    bevor ich aber zum cin.get() komme, wird bereits ein objekt von X zerstört
    gerade das wollte ich nicht. bis zum cin sollen nur zwei objekte erzeugt werden.



  • In Deinem Code ist ein kleiner Fehler enthalten: Der operator+= muss eine Referenz zurückgeben, sonst würde ja das Ergebnis kopiert. x += y gibt aber x zurück.

    Ansonsten hast Du weiterhin recht, dass hier nicht optimiert wird. Deshalb ja auch "schau mal, was Dein Compiler zur benannten Variante sagt". Folgenden Quellcode quittiert der Microsoft VC++ 8.0 im Release-Modus mit "erzeuge, kopiere, vernichte, vernichte". Hier ist keine überflüssige Kopie vorhanden. (Den operator= habe ich gerade auch noch kurz reingebracht, wird nicht aufgerufen)

    class X { 
    public: 
        int n; 
    	X(int n) : n(n) { cout << "erzeuge " << this << endl; }
    	X(X const& x) : n(x.n) { cout << "kopiere " << this << " aus " << &x << endl; }
        ~X() { cout << "vernichte " << this << endl; } 
        X& operator +=(int rhs) { 
            n += rhs; 
            return *this; 
        } 
        X operator +(int rhs) { 
    		X x(*this);
    		x += rhs;
            return x;
        } 
    }; 
    
    int main() 
    { 
        X x = X(2); 
        X z = x + 3; 
        cin.get(); 
    }
    

    EDIT:
    Das Kompilat vom gcc in der Version 3.3.5 erzeugt aus dieser Variante sogar ohne explizite Optimierungsflags die o.a. Ausgabe.



  • okay, ich danke. soweit ist das problem dann gelöst.

    aber warum kommt es im release mode zu einer ausgabe wie z.B.

    erzeuge 0012FF6C
    kopiere 0012FF70 aus 0012FF6C

    vernichte 0012FF70
    vernichte 0012FF6C

    und im debug mode zu:

    erzeuge 0012FF54
    kopiere 0012FE44 aus 0012FF54
    kopiere 0012FF48 aus 0012FE44
    vernichte 0012FE44

    vernichte 0012FF48
    vernichte 0012FF54

    wird das rvo im debug mode nicht angewandt?



  • Im Debug-Mode sind üblicherweise (fast) alle Optimierungen ausgeschaltet. Das macht das Debuggen einfacher, denn wie soll man den Inhalt einer temporären Variable überprüfen, wenn sie bereits wegoptimiert wurde. Die meisten Optimierungen behindern das Debuggen. Das führt allerdings dazu, dass man im Debugmodus noch nicht entscheiden kann, welche Teile im Programm performancekritisch sind.



  • wird das rvo im debug mode nicht angewandt?

    Bingo 🙂


Anmelden zum Antworten