Rückgabe von rValue-Referenzen in virtuellen Funktionen



  • Ich werde dann wohl zukünftig, wenn es wirklich performance-technisch notwendig
    ist, auf Heap-Objekte in einem unique_ptr zurückgreifen:

    #include <iostream>
    #include <memory>
    
    class MoveObject{};
    
    class A{
    public:
            virtual std::unique_ptr<MoveObject> get(){
                    return std::unique_ptr<MoveObject>(new MoveObject());
            }
    };
    
    class B : public A{
    public:
            B() : obj(new MoveObject()){
            }
    
            virtual std::unique_ptr<MoveObject> get(){
                    return std::move(obj);
            }
    private:
            std::unique_ptr<MoveObject> obj;
    };
    
    void move_it(A& t){
            std::unique_ptr<MoveObject> o(t.get());
    }
    
    int main(int argc, char** arg){
            A a;
            move_it(a);
            B b;
            move_it(b);
            return 0;
    }
    


  • XSpille schrieb:

    Ich werde dann wohl zukünftig, wenn es wirklich performance-technisch notwendig
    ist, auf Heap-Objekte in einem unique_ptr zurückgreifen:

    Wenn MoveObject movable ist, bringt das aber nichts. Durch die dynamische Allokation auf dem Freestore kann das Programm sogar langsamer werden.



  • Nexus schrieb:

    XSpille schrieb:

    Ich werde dann wohl zukünftig, wenn es wirklich performance-technisch notwendig
    ist, auf Heap-Objekte in einem unique_ptr zurückgreifen:

    Wenn MoveObject movable ist, bringt das aber nichts. Durch die dynamische Allokation auf dem Freestore kann das Programm sogar langsamer werden.

    Aber sobald eine Funktion virtual ist, kann der Kompiler (meiner Meinung nach)
    keine Aussage mehr darüber treffen, ob er das Objekt moven kann oder kopieren
    muss...
    EDIT: Zumindest, wenn es movebar und kopierbar ist.

    Sehe ich das falsch? 😕
    Natürlich werde ich es nicht bei 'kleinen' Objekten verwenden.
    Über den Overhead einer dynamischen Allokation bin ich mir bewußt....



  • XSpille schrieb:

    Aber sobald eine Funktion virtual ist, kann der Kompiler (meiner Meinung nach)
    keine Aussage mehr darüber treffen, ob er das Objekt moven kann oder kopieren
    muss...

    Das hat nichts mit virtuellen Funktionen zu tun. Ob man Objekte "moven" kann, hängt nur von der Klassendefinition ab, also, ob die Klasse Move-Ctor und Move-Assignment anbietet oder nicht.

    XSpille schrieb:

    Sehe ich das falsch? 😕

    Ja.

    unique_ptr<> kann man auch nicht kopieren un trotzdem kannst Du das Ding von einer virtuellen Funktion zurückgeben.



  • XSpille schrieb:

    Aber sobald eine Funktion virtual ist, kann der Kompiler (meiner Meinung nach)
    keine Aussage mehr darüber treffen, ob er das Objekt moven kann oder kopieren
    muss...
    EDIT: Zumindest, wenn es movebar und kopierbar ist.

    Müsste IMO egal sein.
    Wichtig ist nur, dass der Return-Typ vollständig bekannt ist, und das ist er ja. Einerseits dort wo die Funktion definiert wird, und andrerseits auch dort wo sie aufgerufen wird.

    Selbst mit Covariant-Returns kann man keinen Fall konstruieren der nicht ohne Covariant-Returns und ohne virtual functions auch möglich wäre.

    Warum meinst du dass es einen Unterschied machen würde?



  • hustbaer schrieb:

    Warum meinst du dass es einen Unterschied machen würde?

    Sorry an alle, die ich verwirrt habe 🙄

    Knoten im Kopf... 🕶

    Wer nicht weiter verwirrt werden möchte überspringt den Rest 🤡

    Ich hatte mir eingebildet, dass bei dem Copy- bzw. Move-Konstruktor
    in beiden Fällen, das Original-Objekt ankommt. Im zweiten Fall wird ja,
    wie man auch an der Ausgabe, sieht eine Kopie vor der Rückgabe erstellt. Im Move-Konstruktor
    würde (wenn es so wäre) die Member-Variable dann unbrauchbar gemacht, im Copy-Constructor nicht.
    (Ich hab mir quasi eingebildet, man gibt ne Referenz zurück, was natürlich
    im ersten Fall nicht geht und man ja auch anders in der Signatur angeben kann)
    Naja... etwas verwirrt halt 👎



  • XSpille schrieb:

    Ich hatte mir eingebildet, dass bei dem Copy- bzw. Move-Konstruktor in beiden Fällen, das Original-Objekt ankommt.

    Nein.

    Kleine Analogie (die wie alle Analogien ihre Grenzen hat):
    Siehs wie einen Eimer mit gefärbtem Wasser. Der Eimer ist das Objekt, das Wasser sind die Ressourcen - wenn du nur einen tropfenden kleinen Wasserhahn hast sinds teuere Ressourcen.
    Ein Copy-Ctor würde den Eimer kopieren (neuen Eimer aus dem Schrank holen kostet nicht viel), den dann mit frischem Wasser volltröpfeln (das kostet Zeit!) und das dann genauso färben wie das Original. Der Originaleimer wird weggeworfen, vorher wird dessen gefärbtes Wasser weggekippt (d.h. die Ressourcen wieder freigegeben).

    Ein Move-Ctor kopiert auch Eimer (nimmt also NICHT das Original, neuer Eimer kostet aber ja nicht viel) und kippt das Wasser aus dem alten Eimer in den neuen Eimer. Das darf er, weil der Originaleimer ein rValue ist, d.h. weil der eh gleich weggeworfen wird.

    rValue-Referenzen sind also Referenzen, die extra für Wegwerfeimer gedacht sind 😉

    Nochmal der Klarheit halber: Es kommt nicht das Originalobjekt an - du hast ein neues Objekt, allerdings mit der selben Ressource wie im alten Objekt.

    XSpille schrieb:

    Im Move-Konstruktor würde (wenn es so wäre) die Member-Variable dann unbrauchbar gemacht, im Copy-Constructor nicht.

    Im Grunde stimmt das. Nur geht es beim Moven nicht um x-beliebige Membervariablen sondern um Ressourcen, z.B. allokierten Speicher, Dateihandles, Verbindungen etc. (Ressourcenlose Dinge wie ints kann man nicht moven)
    Im Move-Ctor wird die Ressource des Originalobjektes an das neue Objekt übergeben. Alles weitere (z.B. zusätzliche Daten, wie size und capacity beim vector-Beispiel oben) wird kopiert bzw. semantisch korrekt neu initialisiert.
    Eine Nebenwirkung der ganzen Move-Geschichte ist, dass Destruktoren von movable Klassen den Fall berücksichtigen müssen, dass die Klasse die Ressource nicht mehr besitzt (Eimer leer), weil eben der Fall auftreten kann, dass sie per move-Ctor (oder move-Assignment) bereits an ein anderes Objekt der Klasse übergeben worden ist.



  • Noch ein Zusatz. Das "Umkippen der Eimerinhalte" ist Sache des Klassendesigners. Wie/Was da genau passiert, wird im Move-Ctor definiert. Da steckt keine besondere Compiler-Magie hinter.



  • Lohnt es sich eigentlich, bei der Implementierung des Move-Konstruktors auf ein swap() zurückzugreifen, falls vorhanden? Man hätte möglicherweise ein paar unnötige Operationen, aber dafür ist die Implementierung einfach, semantisch korrekt und verwendet Code wieder.

    Also eine Art Idiom "Move-and-Swap"?



  • Wenn swap() einfach zu implementieren ist, und nichts unnötig rumkopiert, dann würde ich move() auf jeden Fall als swap() implementieren.

    Gründe die dagegen sprechen wären z.B.:
    * swap() muss mehr tun als move(). Möglich z.B. bei String-Klassen mit SSO (small string optimization) - wenn der Ziel-String "small" ist würde swap() dessen Inhalt in den "victim" String kopieren, was bei move() aber nicht nötig ist.
    * man möchte bestimmte Garantien für move() abgeben, wie es z.B. std::unique_ptr tut (victim ist nach move() garantiert NULL).

    Bei Klassen die keinen Default-Ctor haben kann man dadurch auch auf einen "untoten" Spezialzustand verzichten. Wobei sich natürlich die Frage stellt wie sinnvoll movable Types ohne Default-Ctor sind.

    p.S.: mit "move()" meine ich den Move-Assignment-Operator. Der Move-Ctor könnte dann analog erstmal ein "leeres" Objekt erstellen und dann swap() aufrufen. Nur für den Fall dass das nicht ganz klar war. Und das Eliminieren des "untoten" Zustandes funktioniert auch nur, wenn man keinen Move-Ctor sondern nur einen Move-Assignment-Operator implementiert. Zugegebenermassen reichlich fragwürdig 🙂


Anmelden zum Antworten