[gelöst] Vector.push_back und unique_ptr und move semantik



  • Hallo ihr Lieben,

    ich versuche gerade folgendes: Ich hänge wieder an der Speicherverwaltung von C der GSL. Bei folgender Klasse habe ich wie immer bei der GSL das Problem, dass sehr viel mit allokiertem Speicher gearbeitet wird, also schiebe ich das in unique_ptr . Leider lässt sich das ganze nicht, kopieren ... aber auch nicht so gedacht, deshalb setze ich auf die move Semantik, d.h. der Move Konstruktor verschiebt den Inhalt der Pointer.

    Kurioser Weise hat es nicht gereicht den Kopier Konstruktor als private zu erklären, ich musste dem Move Konstruktor noch ein noexcept verpassen. 😕

    Damit funktioniert das Minimalbeispiel weiter unten.

    Was mich jetzt im Nachhinein allerdings wundert: Wenn ich ein push_back verwende, dann kann es doch sein, dass Inhalt des Vektors (um)kopiert werden muss, weil mehr zusammenhängender Platz benötigt wird. Wird dann auch automatisch die move Semantik verwendet?

    Ich habe nämlich im 'größeren Code' das Problem, dass ich erfolgreich das erste Element in den Vektor geschoeben bekomme, beim zweiten passiert dann allerdings Müll.
    Um so seltsamer finde ich es, dass das unten stehende Minimalbeispiel funktioniert, weil dieselbe Klasse matrix.{h,cpp} zugrunde liegt.

    Ich würde mich also freuen, wenn ihr mir sagt, dass das Design der Klasse nicht okay ist und was ich noch beachten muss bzw. schief gehen kann.

    #ifndef MATRIX_H_
    #define MATRIX_H_
    #include <gsl/gsl_eigen.h>
    #include <gsl/gsl_matrix.h>
    #include <gsl/gsl_math.h>
    #include <iostream>
    #include <memory>
    
    class matrix
    {
    	public:
    		matrix(int);
    		matrix(matrix&&) noexcept;
    		void set(int,int,double);
    		double operator()(int,int);
    
    	private:
    		int _n;
    		std::unique_ptr<gsl_matrix,void(*)(gsl_matrix*)> A;
    
    		matrix(const matrix&);
    		matrix& operator=(const matrix&);
    		matrix& operator=(matrix&&);
    };
    #endif
    
    #include "matrix.h"
    
    matrix::matrix(int n):
    _n(n),
    A(gsl_matrix_alloc(n,n),gsl_matrix_free)
    {}
    
    matrix::matrix(matrix&& rhs) noexcept:
    _n( rhs._n ),
    A( std::move( rhs.A ) )
    { }
    
    void matrix::set(int n, int m, double x)
    {
    	gsl_matrix_set(A.get(),n,m,x);
    }
    
    double matrix::operator()(int n,int m)
    {
    	return gsl_matrix_get(A.get(),n,m);
    }
    

    Und das ganze soll in folgendem Minimalbeispiel verwendet werden:

    #include <iostream>
    #include <vector>
    
    #include "matrix.h"
    
    int main()
    {
    
    	std::vector<matrix> vec;
    
    	matrix A( 2 );
    
    	A.set(0,0,1);
    	A.set(0,1,2);
    	A.set(1,0,3);
    	A.set(1,1,4);
    
    	vec.push_back( std::move( A ) );
    
    	matrix B( 2 );
    
    	B.set(0,0,2);
    	B.set(0,1,3);
    	B.set(1,0,4);
    	B.set(1,1,5);
    
    	vec.push_back( std::move( B ) );
    
    	std::cout << vec[0](0,0) << std::endl;
    
    	std::cout << vec[1](0,0) << std::endl;
    
    	return 0;
    }
    

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Wenn ich ein push_back verwende, dann kann es doch sein, dass Inhalt des Vektors (um)kopiert werden muss, weil mehr zusammenhängender Platz benötigt wird. Wird dann auch automatisch die move Semantik verwendet?

    Kurze Antwort: Nein, denn push_back hat die starke Exception-Garantie. Heißt, wenn ein Move fehlschlägt, dann darf sich der vector nicht verändert haben, hat er aber. Deshalb kann vector nur dann move verwenden, wenn es noexcept ist.

    Lange Antwort: Spring auf Minute 32



  • Klaus82 schrieb:

    Wenn ich ein push_back verwende, dann kann es doch sein, dass Inhalt des Vektors (um)kopiert werden muss, weil mehr zusammenhängender Platz benötigt wird. Wird dann auch automatisch die move Semantik verwendet?

    Das kommt auf den Typ an. Falls Dein T einen noexcept Move-Ctor hat, dann ja. Wenn nicht, dann nicht. Dazu gibt es das Utensil std::move_if_noexcept . In jedem Fall kann vector<> beim Vergrößern die starke Ausnahmesicherheit geben. Im Fall "nothrow-movable" kann ja nichts schiefgehen. Und sonst fällt der Vektor auf das Kopieren zurück.

    noexcept bei Move bringt also richtig was.



  • Wenn der Typ nicht kopierbar, aber move-bar ist, fällt vector allerdings auf move zurück, selbst wenn es nicht noexcept ist:

    #include <iostream>
    #include <vector>
    
    class test
    {
    public:
    	test() = default;
    	test(test const&) = delete;
    	test& operator =(test const&) = delete;
    
    	test(test&&)
    	{
    		std::cout << "move-ctor\n";
    	}
    	test& operator =(test&&)
    	{
    		std::cout << "move-assign\n";
    		return *this;
    	}
    };
    
    int main()
    {
    	std::vector<test> vec;
    	vec.reserve(2);
    
    	vec.push_back(test());
    	std::cout << "1\n";
    	vec.push_back(test());
    	std::cout << "2\n";
    	vec.push_back(test());
    
    	std::cin.get();
    }
    

    Output:

    move-ctor
    1
    move-ctor
    2
    move-ctor
    move-ctor
    move-ctor



  • Mh,

    erscheint alles sinnvoll.

    Was mich einfach wundert:

    In meinem 'größeren Code' schiebe ich die erste Matrix in den Vektor.

    Alle Werte sind okay.

    Ich schiebe die zweite Matrix (mit anderen Werten) in den Vektor.

    Die Werte des ersten Vektors sind okay, jene des zweiten sind Müll.

    Idee: Bei der zweiten Matrix wurde etwas fehlerhaft berechnet.

    Test: Ich kommentiere die erste Matrix aus und schiebe nur die zweite Matrix in den Vektor.

    Resultat: Die Werte der zweiten Matrix (jetzt als erstes und einziges Elment im Vektor) erscheinen alle sinnvoll. 😕

    Überlegung: Das Hineinschieben der zweiten Matrix nach der ersten funktioniert nicht?

    Gruß,
    -- Klaus.



  • Dein Copy/Move-Konstruktor ist falsch.



  • nwp3 schrieb:

    Dein Copy/Move-Konstruktor ist falsch.

    Okay.

    Ich habe mir das Video von Scott Meyers angesehen und wieder versucht so viel wie möglich zu verstehen.

    In seinem Vortrag kam im Zusammenhang mit den Konstruktoren auch der unique_ptr vor. Wenn ich das richtig verstehe, dann führt das Vorhandensein eines unique_ptr als Klassenvariable dazu, dass kopieren nicht mehr möglich ist, d.h. ich setze den Kopierkonstruktor private (old style) oder versehe ihn mit = delete (new style).

    Ich muss also zum Move Konstruktor übergehen.

    Das Problem ist jetzt allerdings die Verwendung wie z.B. ein vector mit seinem push_back .

    Ich habe den Eindruck, dass Scott Meyer an einer Stelle sinngemäß gesagt hat, dass std::move etwas mit der strong exception guarantee zu tun hat und falls die nicht gegeben ist, dann fällt der Kompiler zurück auf kopieren anstatt 'moven'.

    Ich muss bei meiner vorliegenden Klasse also dafür sorgen, dass stets 'gemoved' wird, oder? Also insbesondere ein Auge darauf haben, was im aktuellen Beispiel std::vector intern mit den Elementen macht, z.B. bei push_back oder wenn er Speicher reallokieren muss?

    Edit:
    Ich nehme an, dass es darum geht:

    but the general idea is that if the move constructor can throw, the vector will have to copy the elements instead.

    Muss ich meinen Move Constructor dann zusammen mit emplace_back() verwenden?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    In meinem 'größeren Code' schiebe ich die erste Matrix in den Vektor.

    Alle Werte sind okay.

    Ich schiebe die zweite Matrix (mit anderen Werten) in den Vektor.

    Die Werte des ersten Vektors sind okay, jene des zweiten sind Müll.

    Idee: Bei der zweiten Matrix wurde etwas fehlerhaft berechnet.

    Test: Ich kommentiere die erste Matrix aus und schiebe nur die zweite Matrix in den Vektor.

    Resultat: Die Werte der zweiten Matrix (jetzt als erstes und einziges Elment im Vektor) erscheinen alle sinnvoll. 😕

    Daraus habe ich geschlossen, dass dein move-Konstruktor etwas falsches tut. Mit falsch meine ich er baut die Matrix nicht korrekt zusammen oder die alte Matrix löscht was was die neue Matrix noch braucht.

    Klaus82 schrieb:

    Ich muss bei meiner vorliegenden Klasse also dafür sorgen, dass stets 'gemoved' wird, oder?

    Ich dachte das tust du schon. Wenn du kein Copy-Konstruktor hast kann vector nicht kopieren und entweder er benutzt den Move-Konstruktor, oder push_back kompiliert nicht. Da es kompiliert muss er move benutzen.

    Ich würde darauf tippen, dass du vergessen hast den unique_ptr zu moven oder einen Pointer nicht auf nullptr gesetzt hast, weshalb die alte Matrix dir die Daten löscht. Kannst du die Matrixklasse mit dem Move-Konstruktor zeigen?
    Btw: Nur weil die Matrix einen unique_ptr hat heißt das nicht dass man sie nicht kopieren kann. Du kannst den unique_ptr zwar nicht kopieren, aber du kannst die Daten, auf die der unique_ptr zeigt kopieren.

    Vielleicht hilft ein Test:

    Matrix m = {1, 2, 3, 4};
    Matrix m2 = {1, 2, 3, 4};
    Matrix m3 = move(m); //nach dieser Zeile auf m zugreifen ist schlecht
    assert(m2 == m3);
    


  • nwp3 schrieb:

    Kannst du die Matrixklasse mit dem Move-Konstruktor zeigen?

    Steht in meinem Startpost. 🙂

    Gruß,
    -- Klaus.



  • Ups, hatte irgendwie den ersten Post verdrängt, zu viel Code. Ich sehe keinen Fehler. Mein nächster Versuch wäre statt die Werte zu vergleichen den Wert des unique_ptrs und _n zu vergleichen.

    #include <iostream>
    #include <vector>
    
    #include "matrix.h"
    
    int main(){ 
        std::vector<matrix> vec;
    
        matrix A(2);
        A.set(0,0,1);
        A.set(0,1,2);
        A.set(1,0,3);
        A.set(1,1,4);
        std::cout << A(0,0) << '\n';
    
        vec.push_back(std::move(A));
    
        matrix B(2);
        B.set(0,0,2);
        B.set(0,1,3);
        B.set(1,0,4);
        B.set(1,1,5);
        std::cout << B.A.get() << B(0,0) << '\n'; //matrix::A public machen
    
        matrix C(std::move(B));
        std::cout << C.A.get() << C(0,0) << '\n';
    
        vec.push_back(std::move(C));
    
        std::cout << vec[0](0,0) << '\n';
        std::cout << vec[1](0,0) << '\n';
    }
    


  • So,

    das Problem befand sich wieder 'vor' der Tastatur. 🙄

    Ich war zu dumm die Matrix sinnvoll zu füllen, sodass natürlich Müll bei manchen Einträgen herauskam.

    War echt etwas verwundert, da ich mir bei std::move doch Mühe gegeben hatte. 🙂

    Aber es war nochmal gut den Vortrag von Scott zu sehen. 🕶

    Gruß,
    -- Klaus.


Anmelden zum Antworten