boost::ptr_vector mit unkopierbaren Objekten



  • ScyllaIllciz schrieb:

    Nur leider darf es ja eben nicht "zerstört" werden, wenn eigentlich ein LValue übergeben wurde.
    Ich darf also trotz RValue-Referenz nicht mehr davon ausgehen, dass das Objekt nicht mehr gebraucht wird. Wo ist also die eigentliche Idee hinter den RValue-Referenzen hin?

    Ich habe das so verstanden, dass man einen LValue übergibt, wenn er sonst nirgends mehr verwindet wird. Somit kann der LValue auch zerstört werden.

    Andersherum. Ein Name (in einem Ausdruck), der sich auf ein Objekt bezieht -- das schließt auch benannte Referenzen ein -- ist immer ein Lvalue-Ausdruck. Temporäre Objekte haben keinen Namen. Ein Funktionsaufruf, welcher per Wert einen vector<int> zurück gibt ist ein Rvalue-Ausdruck. Ein Funktionsaufruf, der eine Lvalue-Referenz zurück gibt ist natürlich auch ein Lvalue-Ausdruck. Ein Lvalue-Ausdruck bezieht sich immer auf ein Objekt. Ein Rvalue-Ausdruck muss sich nicht unbedingt auf ein Objekt im Sinne von C++ beziehen. Rvalues von skalaren Typen (int,double,void*) sind keine Objekte, sondern einfach nur Werte.

    Wenn man jetzt also schon zwischen Lvalues und Rvalues mit diesen zwei Referenztypen unterscheiden kann und durch das "Klauen von Resourcen" bei Rvalues unnötiges Kopieren vermeiden kann, ohne dass es jemanden stören würde, dann brauchen wir auch eine Syntax, um explizit sagen zu können, dass uns der Erhalt des Zustands eines bestimmten Objekts nicht mehr wichtig ist. Dafür ist std::move da.

    ScyllaIllciz schrieb:

    Wie hier beschrieben:

    Moving From Lvalues

    Dieser Abschnitt bezieht sich genau darauf, wie manexplizit aus Lvalues Rvalues machen kann, um zB unnötiges Kopieren zu vermeiden.

    kk



  • @krümelkacker

    Andersherum. Ein Name (in einem Ausdruck), der sich auf ein Objekt bezieht -- das schließt auch benannte Referenzen ein -- ist immer ein Lvalue-Ausdruck. Temporäre Objekte haben keinen Namen. Ein Funktionsaufruf, welcher per Wert einen vector<int> zurück gibt ist ein Rvalue-Ausdruck. Ein Funktionsaufruf, der eine Lvalue-Referenz zurück gibt ist natürlich auch ein Lvalue-Ausdruck. Ein Lvalue-Ausdruck bezieht sich immer auf ein Objekt. Ein Rvalue-Ausdruck muss sich nicht unbedingt auf ein Objekt im Sinne von C++ beziehen. Rvalues von skalaren Typen (int,double,void*) sind keine Objekte, sondern einfach nur Werte.

    Das ist mir klar.

    Das daraus explizit ein RValue gemacht wird ist mir auch klar.

    Aber ich hatte das hier

    In line 8, we call g with an lvalue, which is ineligible for resource-stealing—even though we’re never going to use b again.

    so verstanden, dass "b" danach nicht mehr benutzt werden kann!? Was passiert bei einem erneuten Zugriff auf "b"? So wie ich es in dem Beitrag gelesen/verstanden habe, wird nach der Umwandlung in ein RValue, "b" zerstört, irre ich mich da?

    Anyway, vielen Dank für Deine richtig guten Erklärungen!!!



  • ScyllaIllciz schrieb:

    Aber ich hatte das hier

    In line 8, we call g with an lvalue, which is ineligible for resource-stealing—even though we’re never going to use b again.

    so verstanden, dass "b" danach nicht mehr benutzt werden kann!?

    Ich glaube, Du hast den Text falsch verstanden. Hier nochmal das Beispiel dazu:

    void g(X t);
    
    void f()
    {
        X b;
        g(b);
        …
        g(b);
    }
    

    In Zeile 6 und in Zeile 8 wird g mit b aufgerufen. Und dieses b ist immer ein Lvalue-Ausdruck. Um das Funktions-lokale Objekt t zu erzeugen, wird also in beiden Fällen ein Konstruktor benutzt, der ein Lvalue-Ausdruck vom Typ X akzeptiert. Einen solchen Konstruktor nennen wir Kopierkonstruktor und wird typischerweise mit X(X const&) deklariert. Da aber b in f nach der 8. Zeile sowieso nicht mehr benötigt wird, kann es einem auch egal sein, was mit b passiert. Durch ein explizites move...

    void g(X t);
    
    void f()
    {
        X b;
        g(b);
        …
        g(std::move(b));
    }
    

    ...kann man dafür sorgen, dass t für den letzten Funktionsaufruf von g eventuell "Move-konstruiert" wird. Wenn X also einen Move-Konstruktor hat -- X::X(X&&) -- wird hier jetzt dadurch, dass std::move(b) ein Rvalue-Ausdruck ist, der Move-Konstruktor von X für t verwendet.

    Was in diesem Move-Konstruktor passiert ist ganz allein die Sache des Klassen-Designers. Man kann natürlich in dem Move-Konstruktor das Quellobjekt verändern, wenn das etwas bringt. Erlaubt ist es, weil eine solche Änderung entweder keiner merken kann oder weil der Nutzer dies explizit erlaubt hat (durch std::move oder std::forward). Der Compiler macht hier jedenfalls nichts besonderes. Nach dem Funktionsaufruf wird b automatisch zerstört (Destruktor aufgerufen), wie es immer schon der Fall bei automatischen Objekten war, wenn der Scope verlassen wird...

    Ganz einfaches Beispiel:

    #include <algorithm>
    
    template<class T>
    class unique_ptr
    {
      T* ptr;
    
    public:
      explicit unique_ptr(T* p=0)
      : ptr(p) {}
    
      unique_ptr(unique_ptr&& x)
      : ptr(x.ptr) {x.ptr=0;}
    
      friend void swap(unique_ptr& a, unique_ptr& b)
      { std::swap(a.ptr,b.ptr); }
    
      unique_ptr& operator=(unique_ptr temp) &
      { swap(*this,temp); return *this; }
    
      ~unique_ptr()
      { delete ptr; }
    
      T& operator*() const {return *ptr;}
      T* operator->() const {return ptr;}
    
      explicit operator bool() const {return ptr!=0;}
      bool operator!() const         {return ptr==0;}
    };
    
    #include <iostream>
    using namespace std;
    
    struct big_momma {
      double foo;
      int bar[999];
    };
    
    unique_ptr<big_momma> source()
    {
      unique_ptr<big_momma> up ( new big_momma() );
      up->foo = 3.14159265;
      up->bar[666] = 1729;
      return up;
    }
    
    void sink(unique_ptr<big_momma> up)
    {
      cout << up->foo << ", " << up->bar[666] << '\n';
    }
    
    int main() {
      sink(source());
    }
    

    unique_ptr ist hier ein "Move-Only"-Typ. Man kann Objekte dieses Typs nicht kopieren, sondern nur "umziehen lassen". Damit kann man sie an Funktionen übergeben und aus Funktionen zurückgeben. Es ist aber durch den Move-Konstruktor sichergestellt, dass nie zwei unique_ptr-Instanzen auf dasselbe Objekt zeigen -- es sei denn, man initialisiert zwei unique_ptr-Instanzen mit dem gleichen Zeigerwert selbst. Das ist dann aber ein Nutzer-Fehler und nicht im eigentlichen Sinne. Es wird dadurch sichergestellt, weil beim "Umziehen" der Zeiger ptr im Quellobjekt auf 0 gesetzt wird. Diese Aktion ist unbedenklich, weil es keinen stören kann. Entweder handelte es sich um ein temporäres Objekt oder der Nutzer hat es explizit erlaubt (std::move, std::forward).

    Wie man sieht, muss ich hier auch nirgends wo ein std::move einsetzen. Das ist nur dazu da, explizit aus einem Lvalue ein Rvalue zu machen. Hier ist das nicht notwendig; denn das Objekt "up" in source wurde lokal erzeugt und wird zurückgegeben (und daher als Rvalue im return-Statement behandelt) und der Ausdruck "source()" ist ebenfalls ein Rvalue. Ein schlauer Compiler kann hier sogar die Moves wegoptimieren, so dass nur ein einziges unique_ptr-Objekt erzeugt und dann wieder zerstört wird. Im "schlimmsten Fall" werden drei unique_ptr-Objekte erzeugt, die sich mit der Verwaltung des erzeugten big_momma-Obejekts abwechseln.

    kk



  • @Krümelkacker
    Danke für die Erklärung. Ich habe tatsächlich nicht daran gedacht, dass hier der Kopierkonstruktor aufgerufen wird. Dann ist der Rest auch klar.

    Nochmal vielen Dank!!!


Anmelden zum Antworten