Funktionen mit RValue Referenzen als Parameter



  • Hallo,
    ich habe eine Frage zu Funktionen mit RValue Referenzen als Parameter.
    Gegeben sei eine Klasse MoveClass, die einen int Zeiger und ein int als Daten haben, sie berücksichtigt die Rule of Five und hat noch eine Methode zum Anzeigen des Wert des int Zeigers und eine zum Setzen von Zahlen in einem int-Array auf dem Heap.
    Was bewirkt foo1(std::move(m1)) bzw. foo1(MoveClass&& moved) genau? Ich weiß nicht wann ich Funktionen, die RValue Referenzen als Parameter habe, einsetzen soll. Mir erschließt sich nicht, was ich für ein Vorteil durch dieses Konstrukt ich bekomme.

    #include <iostream>
    
    class MoveClass
    {
        private:
            static constexpr size_t ARRAYSIZE=10;
            int *p;
            int b;
    
        public:
    
        MoveClass():b{4}
        {
            p=new int[ARRAYSIZE];
        }
    
        MoveClass(int i):b{i}
        {
            p=new int[ARRAYSIZE];
        }
    
        // Copy constructor
        MoveClass(const MoveClass &other):b{other.b}
        {
            std::cout <<"Copy constructor"<<std::endl;
            p=new int[ARRAYSIZE];
            for (int i=0; i<ARRAYSIZE; i++)
            {
                p[i]=other.p[i];
            }
        }
    
        //Destructor
        ~MoveClass()
        {
            std::cout <<"Destructor"<<std::endl;
            delete[] p;
            p= nullptr;
        }
    
        MoveClass& operator=(const MoveClass &other)
        {
            std::cout <<"Assignment operator"<<std::endl;
            b=other.b;
            delete[] p;
            p=new int[ARRAYSIZE];
            for (int i=0; i<ARRAYSIZE; i++)
            {
                p[i]=other.p[i];
            }
            return *this;
        }
    
        //MoveConstructor
        MoveClass(MoveClass &&other) noexcept
        {
            std::cout <<"Move Constructor"<<std::endl;
            b=other.b;
            p=other.p;
    
            other.p=nullptr;
        }
    
        MoveClass& operator=(MoveClass&& other) noexcept
        {
            b=other.b;
            delete[] p;
            p=other.p;
            other.p=nullptr;
            return *this;
        }
    
    
        void setArray()
        {
            for (int i=0; i<ARRAYSIZE; i++)
            {
                // mit irgendeiner Zahl initialisieren
                p[i]=4354365;
            }
        }
    
        void print()
        {
            std::cout<< "Adresse von diesem Objekt: "<<this<<std::endl;
            std::cout<< "Adresse von dem Zeiger: "<<&p<<std::endl;
            std::cout<< "Wert von p: "<<p<<std::endl;
            std::cout<< "Adresse von b: "<<&b<<std::endl;
            std::cout << "Wert von b: "<<b<<std::endl;
        }
    
    };
    
    //Aufruf mit RValue
    void aufrufRValueReferenz(MoveClass&& moved)
    {
        std::cout <<"Print in aufrufRValueReferenz"<<std::endl;
        moved.print();
        std::cout <<"Print Ende in aufrufRValueReferenz"<<std::endl<<std::endl;
    }
    
    // Aufruf über Referenz
    void aufrufReferenz(MoveClass& moved)
    {
        std::cout <<"Print in aufrufReferenz"<<std::endl;
        moved.print();
        std::cout <<"Print Ende in aufrufReferenz"<<std::endl;
    }
    
    void aufrufValue(MoveClass moved)
    {
        std::cout <<"Print in aufrufValue"<<std::endl;
        moved.print();
        std::cout <<"Print Ende in aufrufValue"<<std::endl<<std::endl;
    
    }
    
    int main() {
    
       MoveClass m1;
        m1.setArray();
        m1.print();
        std::cout<<std::endl;
        aufrufRValueReferenz(std::move(m1));
        aufrufReferenz(m1);
        aufrufValue(m1);
        m1.print();
    
        return 0;
    }
    


  • Mir erschließt sich nicht, was ich für ein Vorteil durch dieses Konstrukt ich bekomme.

    Das ist nicht verwunderlich. Deine Klasse ist ja auch ohne move constructor/assignment nutzlos bis fehlerhaft.

    Implementiere die Klasse erst mal nur mit copy vernünftig. Das heißt insbesondere, dass du SIZE nicht überall verwendest. Sprechende Namen helfen.

    Dann kannst du dir überlegen, was passiert, wenn du ein Objekt als Kopie an eine Funktion übergibst. Wenn du das Objekt auf der Aufruferseite nicht mehr benötigst, wird eine unnötige und teure Kopie erzeugt. Dann implementierst du die move Funktionen. Anschließend hast du es verstanden.



  • Ich habe die konkrete Implementierung von den Assignment Ops und Konstruktoren hier im Forum rausgelassen und durch ... ersetzt. Habe sie nachgetragen. Hoffe, dass die Implementierung korrekt ist.
    Was meinst Du mit SIZE? Habe SIZE mit ARRAYSIZE ersetzt.



  • So ganz werde ich da nicht schlau. Unten habe ich mal Protokoll drangehängt.
    Die Ausgaben aller Aufrufe bis auf aufrufRValueReferenz(std::move(m1)) sind mir klar. So wie es in dem Protokoll aussieht gibt es keine Unterschied zwischen aufrufReferenz(m1) und aufrufRValueReferenz(std::move(m1)).

    Ursprünglichs Objekt:
    Adresse von diesem Objekt: 0x7ffc2f0b7920
    Adresse von dem Zeiger: 0x7ffc2f0b7920
    Wert von p: 0x5555de580eb0
    Adresse von b: 0x7ffc2f0b7928
    Wert von b: 4

    aufrufRValueReferenz(std::move(m1))
    Print in aufrufRValueReferenz
    Adresse von diesem Objekt: 0x7ffc2f0b7920
    Adresse von dem Zeiger: 0x7ffc2f0b7920
    Wert von p: 0x5555de580eb0
    Adresse von b: 0x7ffc2f0b7928
    Wert von b: 4
    Print Ende in aufrufRValueReferenz

    aufrufReferenz(m1)
    Print in aufrufReferenz
    Adresse von diesem Objekt: 0x7ffc2f0b7920
    Adresse von dem Zeiger: 0x7ffc2f0b7920
    Wert von p: 0x5555de580eb0
    Adresse von b: 0x7ffc2f0b7928
    Wert von b: 4
    Print Ende in aufrufReferenz

    aufrufValue(m1);
    Copy constructor
    Print in aufrufValue
    Adresse von diesem Objekt: 0x7ffc2f0b7930
    Adresse von dem Zeiger: 0x7ffc2f0b7930
    Wert von p: 0x5555de5812f0
    Adresse von b: 0x7ffc2f0b7938
    Wert von b: 4
    Print Ende in aufrufValue

    aufrufValue(std::move(m1));
    Move Constructor
    Print in aufrufValue
    Adresse von diesem Objekt: 0x7ffc2f0b7930
    Adresse von dem Zeiger: 0x7ffc2f0b7930
    Wert von p: 0x5555de580eb0
    Adresse von b: 0x7ffc2f0b7938
    Wert von b: 4
    Print Ende in aufrufValue

    //Ursprüngliches Objekt nach Aufruf des Move-Konstruktors beschädigt, da Wert des Pointer 0 bzw. nullptr
    Adresse von diesem Objekt: 0x7ffc2f0b7920
    Adresse von dem Zeiger: 0x7ffc2f0b7920
    Wert von p: 0
    Adresse von b: 0x7ffc2f0b7928
    Wert von b: 4



  • @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    Was meinst Du mit SIZE? Habe SIZE mit ARRAYSIZE ersetzt.

    Ich hatte angenommen, dass der Konstruktor mit int Parameter ein Array mit anderer Größe anlegen sollte.



  • @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    So ganz werde ich da nicht schlau.

    Ersetzte deine main Funktion durch

    int main() {
    
       MoveClass m1;
       m1.setArray();
       m1.print();
       std::cout<<std::endl;
       aufrufReferenz(m1);
       aufrufValue(m1);
       aufrufRValueReferenz(std::move(m1)); // Reihenfolge! Nach dem move ist p == nullptr!
       m1.print();
    
       std::cout << "======\n";
       
       MoveClass m2;
    
       aufrufValue(std::move(m2)); // Hier wird nicht kopiert!
    
       m2.print();
    }
    

    ersetze aufrufRValueReferenz durch

    void aufrufRValueReferenz(MoveClass&& moved) // hier wird kein neues Objekt erzeugt 
    {
      MoveClass m( std::move(moved)); // erst hier ist der "move" komplett
      std::cout <<"Print in aufrufRValueReferenz"<<std::endl;
      m.print();
      std::cout <<"Print Ende in aufrufRValueReferenz"<<std::endl<<std::endl;
    }
    

    Vielleicht helfen die Kommentare weiter



  • @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    //Destructor
    ~MoveClass()
    {
        std::cout <<"Destructor"<<std::endl;
        delete[] p;
        p= nullptr;
    }
    

    OT: p= nullptr; ist völlig überflüssig. p ist nach } nicht mehr da!



  • @manni66 sagte in Funktionen mit RValue Referenzen als Parameter:

    @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    Was meinst Du mit SIZE? Habe SIZE mit ARRAYSIZE ersetzt.

    Ich hatte angenommen, dass der Konstruktor mit int Parameter ein Array mit anderer Größe anlegen sollte.

    Nö, bezieht sich auf b.



  • @manni66 sagte in Funktionen mit RValue Referenzen als Parameter:

    @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    //Destructor
    ~MoveClass()
    {
        std::cout <<"Destructor"<<std::endl;
        delete[] p;
        p= nullptr;
    }
    

    OT: p= nullptr; ist völlig überflüssig. p ist nach } nicht mehr da!

    Stimmt.



  • @manni66 sagte in Funktionen mit RValue Referenzen als Parameter:

    @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    So ganz werde ich da nicht schlau.

    Ersetzte deine main Funktion durch

    int main() {
    
       MoveClass m1;
       m1.setArray();
       m1.print();
       std::cout<<std::endl;
       aufrufReferenz(m1);
       aufrufValue(m1);
       aufrufRValueReferenz(std::move(m1)); // Reihenfolge! Nach dem move ist p == nullptr!
       m1.print();
    
       std::cout << "======\n";
       
       MoveClass m2;
    
       aufrufValue(std::move(m2)); // Hier wird nicht kopiert!
    
       m2.print();
    }
    

    ersetze aufrufRValueReferenz durch

    void aufrufRValueReferenz(MoveClass&& moved) // hier wird kein neues Objekt erzeugt 
    {
      MoveClass m( std::move(moved)); // erst hier ist der "move" komplett
      std::cout <<"Print in aufrufRValueReferenz"<<std::endl;
      m.print();
      std::cout <<"Print Ende in aufrufRValueReferenz"<<std::endl<<std::endl;
    }
    

    Vielleicht helfen die Kommentare weiter

    Danke! Ich werde mir das gleich in Ruhe anschauen.



  • OK. Danke.
    Dann hätte ich eine weitere Frage.

    // Aufruf über Referenz
    void aufrufReferenz(MoveClass& moved)
    {
        MoveClass m( std::move(moved)); //Neu hinzugkommen
        std::cout <<"Print in aufrufReferenz"<<std::endl;
        moved.print();
        std::cout <<"Print Ende in aufrufReferenz"<<std::endl;
    }
    

    Hätte doch denselben Effekt mit aufrufReferenz(m1)? Einzige Unterschied wäre, dass man nur LValue Referenzen angeben darf. aufrufReferenz(Moveclass(4)) wäre zum Beispiel nicht möglich.



  • @manni66 sagte in Funktionen mit RValue Referenzen als Parameter:

    @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    So ganz werde ich da nicht schlau.

    Ersetzte deine main Funktion durch

    int main() {
    
       MoveClass m1;
       m1.setArray();
       m1.print();
       std::cout<<std::endl;
       aufrufReferenz(m1);
       aufrufValue(m1);
       aufrufRValueReferenz(std::move(m1)); // Reihenfolge! Nach dem move ist p == nullptr!
       m1.print();
    
       std::cout << "======\n";
       
       MoveClass m2;
    
       aufrufValue(std::move(m2)); // Hier wird nicht kopiert!
    
       m2.print();
    }
    

    ersetze aufrufRValueReferenz durch

    void aufrufRValueReferenz(MoveClass&& moved) // hier wird kein neues Objekt erzeugt 
    {
      MoveClass m( std::move(moved)); // erst hier ist der "move" komplett
      std::cout <<"Print in aufrufRValueReferenz"<<std::endl;
      m.print();
      std::cout <<"Print Ende in aufrufRValueReferenz"<<std::endl<<std::endl;
    }
    

    Vielleicht helfen die Kommentare weiter

    Wenn ich richtig verstanden habe, ist der Sinn für

    void aufrufRValueReferenz(MoveClass&& moved)
    

    den Move-Konstruktor bzw. Move Assignment in der Funktion einzusetzen. Ist das richtig?



  • @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

        // Copy constructor
        MoveClass(const MoveClass &other):b{other.b}
        {
            std::cout <<"Copy constructor"<<std::endl;
            p=new int[ARRAYSIZE];
            for (int i=0; i<ARRAYSIZE; i++)
            {
                p[i]=other.p[i];
            }
        }
    
    // ...
    
        MoveClass& operator=(const MoveClass &other)
        {
            std::cout <<"Assignment operator"<<std::endl;
            b=other.b;
            delete[] p;
            p=new int[ARRAYSIZE];
            for (int i=0; i<ARRAYSIZE; i++)
            {
                p[i]=other.p[i];
            }
            return *this;
        }
    

    Fällt dir auf daß da was doppelt ist? Ist auch nicht exception-safe. Google mal nach dem Copy and Swap Idiom.



  • @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    Hätte doch denselben Effekt mit aufrufReferenz(m1)?

    Ja. Man sieht dem Funktionsaufruf aber nicht an, dass das Objekt danach nicht mehr zu gebrauchen ist.



  • @asd1 sagte in Funktionen mit RValue Referenzen als Parameter:

    den Move-Konstruktor bzw. Move Assignment in der Funktion einzusetzen. Ist das richtig?

    Man kann auch eine andere Funktion aufrufen, die die Referenz annimmt.


Log in to reply