[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 diemove
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 einnoexcept
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 diemove
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 Klassematrix.{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 diemove
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 diemove
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 einesunique_ptr
als Klassenvariable dazu, dass kopieren nicht mehr möglich ist, d.h. ich setze den Kopierkonstruktorprivate
(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 seinempush_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. beipush_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.