Move-Konstruktor



  • Hey Leute,

    ich habe ein Programm bestehend aus einer Klasse Measure zur Darstellung eines Messwertes und einer Klasse MeasureArr zur Darstellung eines Vektors für Messwerte. Die Klasse MeasureArr besitzt ein dynamisches Element msPtr, welches Objekte vom Typ Measure adressiert, und steht darüber hinaus diverse Konstruktoren und Operatoren zur Verfügung:

    Header.h

    class Measure
    {
    	private:
    		double reading;
    
    	public:
    		Measure(double rd= 0.0): reading(rd){}		
    };
    
    class MeasureArr
    {
    	private:
    		Measure* msPtr;
    		int max;
    		int anz;
    
    	public:
    		MeasureArr(int mx) : max(mx), anz(0)
    		{
    			msPtr = new Measure[max];
    		}
    
    		MeasureArr(MeasureArr&&); // Move-Konstruktor		
    
    		MeasureArr(const MeasureArr&); // Copy-Konstruktor		
    
    		MeasureArr& operator= (MeasureArr&&); // Move-Zuweisung
    		
    		MeasureArr& operator= (const MeasureArr&); // Zuweisung
    		
    		~MeasureArr();	
    
    		void append(const Measure&);		
    
    		Measure& operator[] (int);				
    };
    

    Quelle.cpp:

    MeasureArr::~MeasureArr()
    {
    	delete[] msPtr;
    }
    
    MeasureArr& MeasureArr::operator= (const MeasureArr& msAr)
    {
    	if (this != &msAr)
    	{
    		max = msAr.max;
    		anz = msAr.anz;
    		delete[] msPtr;
    		msPtr = new Measure[max];
    
    		for (int i = 0; i < max; i++)
    		{
    			msPtr[i] = msAr.msPtr[i];
    		}
    	}
    
    	return *this;
    }
    
    MeasureArr& MeasureArr::operator=(MeasureArr&& msAr)
    {
    	Measure* p = msPtr;
    	msPtr = msAr.msPtr;
    	msAr.msPtr = p;
    	max = msAr.max;
    	anz = msAr.anz;
    
    	return *this;
    }
    
    MeasureArr::MeasureArr(const MeasureArr& msAr)
    {
    	max = msAr.max;
    	anz = msAr.anz;
    	msPtr = new Measure[max];
    
    	for (int i = 0; i < anz; i++)
    	{
    		msPtr[i] = msAr.msPtr[i];
    	}
    }
    
    MeasureArr::MeasureArr(MeasureArr&& msAr)
    {
    	max = msAr.max;
    	anz = msAr.anz;
    	msPtr = msAr.msPtr;
    	msAr.msPtr = nullptr;
    }
    
    Measure& MeasureArr::operator[] (int index)
    {
    	if (index < max)
    	{
    		return msPtr[index];
    	}
    }
    
    void MeasureArr::append(const Measure& rd)
    {
    	if (anz + 1 <= max)
    		msPtr[anz + 1] = rd;
    }
    

    Zum Testen des Codes habe ich nun verschiedene Objekte angelegt und die Funktion test verwendet.

    Main:

    MeasureArr test(const MeasureArr& msAr)
    {
        MeasureArr Obj1(msAr);
        Obj1[1] = Measure(77);
    
        return Obj1;
    }
    
    int main()
    {
        MeasureArr test1(2.0), test2(4.5);   
    
        test2 = test(test1);
    
        test2[0] = 9.9;   
    }
    

    Beim Debuggen der Funktion test habe ich gesehen das zuerst der Kopierkonstruktor, mit return der Move-Konstruktor
    und zuletzt die Move-Zuweisung aufgerufen werden. Das da der Kopierkonstruktor und die Move-Zuweisung aufgerunfen
    werden ist klar. Warum aber mit return auch noch der Move-Konstruktor des zu diesen Zeitpunkt bestehenden Objektes Obj1?



  • Ok, ich habe auf die Schnelle ein paar Dinge verändert, damit das besser passt. Die Standard Frage, weshalb kein std::vector? Und wenn man schon selbst die Rule of Five nutzt, dann die Konstruktoren auch richtig ausformulieren.

    P.S. Ich hoffe ich habe nichts übersehen. clang und g++ meinen es sei so ok.

    Nachtrag:
    Code korrigiert, und noch etwas ergänzt.

    #include <algorithm>
    #include <stdexcept>
    #include <string_view>
    #include <cstring>
    #include <utility>
    #include <iostream>
    
    #include "demangle.h"
    
    using std::swap, std::move;
    
    class Measure {
    	double reading;
    public:
    	Measure() : reading (0.0) {}
    	explicit Measure(const double rd) : reading(rd) {}
    };
    
    class MeasureArr {
    	Measure* msPtr;
    	size_t max;
    	size_t anz;
    public:
    	MeasureArr(const size_t mx) : msPtr(new Measure[mx]), max(mx), anz(0) {
    		std::cout << "Konstruktor\n";
    		std::cout << "type of msPtr    " << demangle(typeid(msPtr).name()) << "\n";
    		std::cout << "type of msPtr[i] " << demangle(typeid(msPtr[0]).name()) << "\n";
    	}
    
    	~MeasureArr ();
    	MeasureArr (MeasureArr&&); // Move-Konstruktor		
    	MeasureArr (const MeasureArr&); // Copy-Konstruktor		
    	MeasureArr& operator= (MeasureArr&&); // Move-Zuweisung
    	MeasureArr& operator= (const MeasureArr&); // Kopier-Zuweisung
    	
    	void append(const Measure&);
    
    	Measure& operator[] (const size_t);				
    };
    
    MeasureArr::~MeasureArr() {
    	std::cout << "Destruktor\n";
    	if (msPtr) delete[] msPtr;
    }
    
    MeasureArr& MeasureArr::operator= (const MeasureArr& msAr) {
    	std::cout << "Kopierzuweisung\n";
    	if (this != &msAr) {
    		MeasureArr T(msAr);
    		
    		swap (T, *this);
    	}
    
    	return *this;
    }
    
    MeasureArr& MeasureArr::operator= (MeasureArr&& msAr) {
    	std::cout << "Move-Zuweisung\n";
    
    	if (this != &msAr) {
    		delete[] this->msPtr;
    
    		this->msPtr = move(msAr.msPtr);
    		this->anz   = move(msAr.anz);
    		this->max   = move(msAr.max);
    
    		msAr.msPtr = nullptr;
    		msAr.anz = 0;
    		msAr.max = 0;
    	}
    
    	return *this;
    }
    
    MeasureArr::MeasureArr (const MeasureArr& msAr) : msPtr(new Measure[msAr.max]), max(msAr.max), anz(msAr.anz)  {
    	std::cout << "Kopierkonstruktor\n";
    	std::copy_n (msAr.msPtr, anz, msPtr);
    }
    
    MeasureArr::MeasureArr (MeasureArr&& msAr) : msPtr(msAr.msPtr), max (msAr.max), anz (msAr.anz) {
    	std::cout << "Move-Konstruktor\n";
    	msAr.msPtr = nullptr;
    	msAr.max = 0;
    	msAr.anz = 0;
    }
    
    Measure& MeasureArr::operator[] (const size_t index) {
    	if (index >= max) throw std::runtime_error("out of bounds");
    
    	return msPtr[index];
    }
    
    void MeasureArr::append(const Measure& rd) {
    	if (anz+1 > max) throw std::runtime_error("no space left");
    
    	msPtr[anz] = rd;
    	anz++;
    }
    
    MeasureArr test (const MeasureArr& msAr) {
        MeasureArr Obj1(msAr);
        Obj1[0] = Measure(77);
    
        return Obj1;
    }
    
    int main() {
        //MeasureArr test1(2.0), test2(4.5);
        // das ist so Unsinn
        MeasureArr test1(2), test2(4);
    
        test2 = test(test1);
    
        test2[0] = Measure(9.9);   
    }
    

    Code für demangle.h

    #ifndef DEMANGLE_H
    #define DEMANGLE_H
    
    #include <type_traits>
    #include <string>
    
    #ifdef __GNUG__
    #include <cxxabi.h>
    	using GNU_COMPILER_TOOLCHAIN = std::true_type;
    #else
    	using GNU_COMPILER_TOOLCHAIN = std::false_type;
    #endif
    
    std::string demangle (const std::string& str);
    
    #endif
    

    und demangle.cc

    #include "demangle.h"
    #include <string>
    
    std::string demangle (const std::string& str) {
    	if constexpr (std::is_same_v<GNU_COMPILER_TOOLCHAIN, std::true_type>) {
    		size_t length = str.length();
    		int status = 0;
    		return abi::__cxa_demangle (str.c_str(), nullptr, &length, &status);
    	} else {
    		return str;
    	}
    }
    
    std::string demangleType (auto& v) {
    	return demangle(typeid(decltype(v)).name());
    }
    


  • @C-Sepp sagte in Move-Konstruktor:

    Warum aber mit return auch noch der Move-Konstruktor des zu diesen Zeitpunkt bestehenden Objektes Obj1?

    Beim return wird ein temporäres Objekt mit dem Move-Konstruktor als Rückgabe-Wert der Funktion erzeugt. Als nächstes wird dann Obj1 zerstört.
    Jetzt erst wird mit der move-Zuweisung das temporäre Objekt an test2 zugewiesen.



  • @john-0 sagte in Move-Konstruktor:

    Ok, ich habe auf die Schnelle ein paar Dinge verändert, damit das besser passt. Die Standard Frage, weshalb kein std::vector? Und wenn man schon selbst die Rule of Five nutzt, dann die Konstruktoren auch richtig ausformulieren.

    Auf die Schnelle: warum memcpy im copy-Constructor? Das erfordert, dass die Klasse Measure keine speziellen Dinge im Konstruktor tut, keine besitzenden Pointer oder Pointer auf sich selbst enthält usw. Gut, das ist hier der Fall, aber ich schätze mal, dass das hier eh nur ein Toy-Example ist. Zumindest so ganz ohne Warnung wollte ich das nicht einfach so stehen lassen.

    Ansonsten an @C-Sepp: du solltest dir mal hier: https://en.cppreference.com/w/cpp/language/exceptions den Abschnitt zu "Exception safety" durchlesen. Du bietest keine Garantien. Durch Dinge wie copy & move von @john-0 sieht das besser aus. Lies dir vielleicht auch mal Doku zu https://en.cppreference.com/w/cpp/language/noexcept_spec durch.

    Und dann verstehe ich die append-Methode nicht. Weder bei @C-Sepp noch bei @john-0:

    void MeasureArr::append(const Measure& rd)
    {
    	if (anz + 1 <= max)
    		msPtr[anz + 1] = rd;
    }
    

    Würde man nicht erwarten, dass sich der interne anz-Wert um 1 erhöht? Würde man nicht auch erwarten, dass im Fall anz == max (besser anz < max testen als anz + 1 <= max!) irgendwas sinnvolles passiert?



  • Max steht für die maximale Anzahl an Vektorelementen, anz für die aktuelle Anzahl. Die Funktion append() soll einen Messwert hinter den vorhandenen Elementen im Vektor einfügen. Richtiger wäre sicherlich:

    void MeasureArr::append(const Measure& rd)
    {
    	if (anz + 1 <= max)
    		msPtr[anz++] = rd;
    }
    

    Das Objekt rd vom Typ Measure wird in dieser Zuweisung ja den Zeiger msPtr, welcher ebenfalls vom Typ Measure ist, zugewiesen. Warum wird in dem Fall weder der Zuweisungsoperator = noch die Move-Zuweisung ausgeführt?
    Woher weis ich, wann die Move-Zuweisung, wann der Zuweisungsoperator = ausgeführt wird? Wird die Move-Zuweisung manchmal nur für R-Werte ausgeführt. Nochmals vielen Dank!



  • @C-Sepp

    Zeile 3 ist ungewöhnlich, stattdessen besser

    void MeasureArr::append(const Measure& rd)
    {
      if (anz < max)
          msPtr[anz++] = rd;
    }
    

    Hier ist ein Proposal zur move-Semantik, da steht auch drin, wann move greift.



  • @wob sagte in Move-Konstruktor:

    Auf die Schnelle: warum memcpy im copy-Constructor?

    Das geht halt nur im Falle von PODs, was hier der Fall ist.

    Und dann verstehe ich die append-Methode nicht. Weder bei @C-Sepp noch bei @john-0:

    Das hatte ich übersehen.



  • Dieser Beitrag wurde gelöscht!


  • @john-0 sagte in Move-Konstruktor:

    Das geht halt nur im Falle von PODs, was hier der Fall ist.

    Ich würde trotzdem vor dem memcpy folgendes einfügen:

    static_assert(std::is_trivially_copyable<Measure>::value, "Measure must be trivially copyable!");
    

    Man sieht der Klasse Measure nämlich nicht an, das sie trivially copyable sein muss. Und zu schnell hat man die Klasse um ein nicht POD Objekt (z.B. std::string) erweitert.

    Deswegen der static_assert. So klopft einem der Compiler auf die Finger falls Measure nicht trivially copyable ist und deswegen memcpy ein UB zurückliefert.



  • Kann man das nicht dem Compiler überlassen? Der kennt doch den Datentyp und ich habe schon Gerüchte gehört, dass der Compiler automatisch die optimale Kopiermethode auswählt.
    Man könnte im Compiler Explorer mal sehen, welchen Code welcher Compiler erzeugt.



  • @Quiche-Lorraine sagte in Move-Konstruktor:

    Ich würde trotzdem vor dem memcpy folgendes einfügen:

    Dann lieber so

    MeasureArr::MeasureArr (const MeasureArr& msAr) : msPtr(new Measure[msAr.max]), max(msAr.max), anz(msAr.anz)  {
    	std::cout << "Kopierkonstruktor\n";
    	std::copy_n (msAr.msPtr, anz, msPtr);
    }
    


  • Vielen Dank...damit kommt mit Sicherheit so schnell keine Langeweile auf :D.
    Welche Frage sich jetzt noch ergeben hat:
    Warum wird in der Funktion append() den dynamischen Element msPtr[] eigentlich nicht mit new das Objekt rd zugewiesen,
    so wie es auch im Konstruktor gemacht wurde? (msPtr[] = new rd) Danke!



  • Vergleiche mal beide Datentypen bzw. schau dir mal deinen eigenen Kopierkonstruktor oder operator = an...



  • @C-Sepp sagte in Move-Konstruktor:

    Warum wird in der Funktion append() den dynamischen Element msPtr[] eigentlich nicht mit new das Objekt rd zugewiesen, so wie es auch im Konstruktor gemacht wurde? (msPtr[] = new rd) Danke!

    Nein, aus gutem Grund hatte ich den Konstruktor verändert.

    MeasureArr(const size_t mx) : msPtr(new Measure[mx]), max(mx), anz(0) {}
    

    Es wird hier dem Pointer msPtr der Wert aus der Allokation new Measure[mx] zugewiesen. Es wird hier keinerlei Objekt des Typs Measure zugewiesen, sondern nur Speicher für die Anzahl mx an Measure Objekte angefordert. Später kann man dann mit append Measure Objekte in dem bereits allozierten Speicher ablegen.



  • @john-0 sagte in Move-Konstruktor:

    Es wird hier keinerlei Objekt des Typs Measure zugewiesen, sondern nur Speicher für die Anzahl mx an Measure Objekte angefordert.

    Wie kommst du darauf?
    Es wird hier ein Array erzeugt und für jedes Array-Element dessen Standardkonstruktor (hier also Measure()) aufgerufen.

    PS: In deinem Code in Zeile 23 fehlen Kommentarzeichen.



  • Wusst ich's doch. Analog zu string*pStr = new string(20,'X'); könnte man nämlich auch
    msPtr = new Measure(1, 2); in der Funktion schreiben. Jetzt stellt sich nur noch die Frage
    warum bei msPtr[1] = new Measure(1, 2); immer eine Fehlermeldung kommt?



  • Welchen Datentyp hat msPtr und welchen msPtr[i]?

    Außerdem mußt du zwischen statischem und dynamischen Datentyp (zur Laufzeit) unterscheiden.



  • @Th69 sagte in Move-Konstruktor:

    Wie kommst du darauf?

    In append wird ein Wert als Parameter übergeben im Konstruktor nicht.



  • Ah stimmt...msPtr ist ja vom Typ Zeiger auf Measure, msPtr[i] spricht wiederrum den Wert an auf den msPtr[i] zeigt.



  • Wie bereits erwähnt, wird mit msPtr[i] = rd ja ein Wert an msPtr übergeben. Welcher Wert ist das denn...eigentlich wird doch ein Objekt als Parameter übergeben? Wenn das der Wert des Membervariablen des üergebenen Objektes ist, wie verhält sich das dann für mehrere Membervariablen? Danke!


Anmelden zum Antworten