Copy And Swap Implementation ok?



  • In meinem Aufgabenbereich benötige ich explizit eine Vertauschfunktion. Also so mein Gedanke könnte ich auch bereits vorhandene Klassen auf das Copy and Swap Idiom umstellen. Da viele Klassen noch viel doppelten Code nutzen (einige Klassen haben stellenweise bis zu 5 Konstruktoren welche alle Member der Klasse explizit initialisieren) würde die Kombination aus direkter Initialisierung der Klassen-Member und Copy and Swap Idiom viel Code einsparen.

    Leider bin ich in beiden Bereichen noch etwas unerfahren. Daher die Frage: Ist der folgende Code ok?

    #include <vector>
    
    	
    class T
    {
    private:
    	std::vector<int> mData;
    	std::string mName = "Default";
    	int mStartValue = -1;
    
    public:
    	T() {};		// Kann ich hier davon ausgehen das mName "Default", mStartValue -1 und mData leer ist?
    
    	T(const std::string& Name, int t) :
    		mName(Name), 
    		mStartValue(t)
    	{
    		for (int i = 0; i < 3; i++)
    			mData.push_back(t + i);
    	}
    
    
    	T(int t) :
    		mStartValue(t)			
    	{
    		// Kann ich hier davon ausgehen das mName "Default" und mData leer ist?
    		for (int i = 0; i < 3; i++)
    			mData.push_back(t + i);
    	}
    
    	
    	T(T&& l) throw() :   // VS 2013 kann leider noch kein noexcept
    		T()	
    	{
    		swap(*this, l);
    	}
    	
    
    	static
    	void swap(T& lhs, T& rhs)
    	{
    		std::swap(lhs.mData, rhs.mData);
    		std::swap(lhs.mName, rhs.mName);
    		std::swap(lhs.mStartValue, rhs.mStartValue);
    	}
    
    	T& operator=(T Value)
    	{
    		swap(*this, Value);
    		return *this;
    	}
    };
    
    
    
    // Testcode zum debuggen
    int main(int argc, char* argv[])
    {
    	T z(5);			
    	T a("Hallo", 4);
    	T b("Welt", 5);
    
    	T c(b);
    	T d;
    
    	d = a;	
    	return 0;
    }
    
    

  • Mod

    Korrekt wäre, diese Operationen allesamt komplett zu streichen. Deine Klasse verwaltet keine Ressourcen. Das tun der String und der Vector, aber die machen das schon korrekt und du brauchst/darfst gar nichts mehr selber zu machen.

    Stichworte dazu: Rule of 3 bzw. Rule of 5 (um zu sehen, warum diese hier nicht zutreffen), oder in diesem Fall Rule of 0 (für das, was ich gesagt habe).



  • @Quiche-Lorraine sagte in Copy And Swap Implementation ok?:

    // Kann ich hier davon ausgehen das mName "Default", mStartValue -1 und mData leer ist?
    

    Ja.

    #include <string>
    #include <vector>
    #include <numeric>
    
    class T {
    	std::vector<int> mData;
    	std::string mName = "Default";
    	int mStartValue = -1;
    
    public:
    	T() = default;
    
    	explicit
    	T(int t, std::string Name = "Default")
    	: mData(3),
    	  mName{ Name },
    	  mStartValue{ t }
    	{
    		std::iota(std::begin(mData), std::end(mData), mStartValue);
    	}
    };
    

  • Mod

    PS: Das hier ist ganz fies für deine Nutzer:

    T(const std::string& Name, int t) :
    ...
    T(int t) :
    

    Lass die Reihenfolge der Parameter unbedingt gleich, solange sie das gleiche bedeuten (was sie hier ja tun)! Außerdem sind die beiden Funktionen sowieso identisch, bis darauf, dass Name optional ist. Du kannst dir also die Codeverdoppelung sparen und zudem nutzerfreundlicher sein, indem du nur einmal

    	T(int t, const std::string& Name = "Default") :
    		mName(Name), 
    		mStartValue(t)
    	{
    		for (int i = 0; i < 3; i++)
    			mData.push_back(t + i);
    	}
    

    schreibst.

    edit: Da war Swordfish schneller und hat noch mehr verbessert.



  • swap sollte kein static member, sondern eine Spezialisierung von std::swap sein. Damit kommen dann alle Algorithmen und Container der STL in den Genuss der Implementation. Also

    namespace std
    {
       template<>
       void swap( T& lhs, T& rhs )
       {
          // mach wat
       }
    }
    


  • @SeppJ sagte in Copy And Swap Implementation ok?:

    edit: Da war Swordfish schneller und hat noch mehr verbessert.

    Dafür sind andere besser im Erklärungenschreiben ^^

    // edit: Was man über Copy'nSwap wissen muss



  • @Swordfish sagte in Copy And Swap Implementation ok?:

    	> 	explicit
    	T(int t, std::string Name = "Default")  // parameter Name per value
    	: mData(3),
    	  mName{ std::exchange(mName, Name) },  // damit ein bisschen copy'n swap bleibt
    	  mStartValue{ t }
    	{
    		std::iota(std::begin(mData), std::end(mData), mStartValue);
    	}
    

    mName{ std::exchange(mName, Name) } -- huh???

    Du greifst hier vor/für/während der Initialisierung von mName auf mName zu. Ich behaupte einfach mal: das kann so nicht gehen.



  • @hustbaer sagte in Copy And Swap Implementation ok?:

    Ich behaupte einfach mal: das kann so nicht gehen.

    uups



  • Eine kleine Anmerkung zu Copy-and-Swap vs. Rule-Of-Zero:
    Ein (korrekter) Copy-and-Swap Copy-Assignment Operator hat die Strong Exception Guarantee (all or nothing).
    Ein Rule-Of-Zero Copy-Assignment Operator hat oft nur die Weak Exception Guarantee (undefined content, OK to destruct without undefined behavior).

    Vielleicht wäre das was für eine "rule of one"?



  • @DocShoe sagte in Copy And Swap Implementation ok?:

    swap sollte kein static member, sondern eine Spezialisierung von std::swap sein. Damit kommen dann alle Algorithmen und Container der STL in den Genuss der Implementation. Also

    namespace std
    {
       template<>
       void swap( T& lhs, T& rhs )
       {
          // mach wat
       }
    }
    

    Macht man das nicht einfach im Namensraum von T:

    void swap( T& lhs, T& rhs )
    {
       // mach wat
    }
    

    ?



  • @hustbaer sagte in Copy And Swap Implementation ok?:

    Eine kleine Anmerkung zu Copy-and-Swap vs. Rule-Of-Zero:
    Ein (korrekter) Copy-and-Swap Copy-Assignment Operator hat die Strong Exception Guarantee (all or nothing).

    Das hängt von POCCA, POCMA, POCS und IAE ab des verwendeten Allocators ab, sind die jeweils false ist die noexcept rule nicht mehr garantierbar oder man hat UB. Alle Container der Standard Library haben mit solchen Allokatoren UB.

    POCCA = propagate_on_container_copy_assigment
    POCMA = propagate_on_container_move_assigment
    POCS = propagate_on_container_swap
    IAE = is_always_equal



  • @john-0: Da fehlt wohl eine Leerzeile nach dem Zitat (denn der letzte Satz ist doch wohl von dir?).

    @john-0 sagte in Copy And Swap Implementation ok?:

    @hustbaer sagte in Copy And Swap Implementation ok?:

    Eine kleine Anmerkung zu Copy-and-Swap vs. Rule-Of-Zero:
    Ein (korrekter) Copy-and-Swap Copy-Assignment Operator hat die Strong Exception Guarantee (all or nothing).

    Das hängt von POCCA, POCMA, POCS und IAE ab des verwendeten Allocators ab, sind die jeweils false ist die noexcept rule nicht mehr garantierbar oder man hat UB. Alle Container der Standard Library haben mit solchen Allokatoren UB.

    POCCA = propagate_on_container_copy_assigment
    POCMA = propagate_on_container_move_assigment
    POCS = propagate_on_container_swap
    IAE = is_always_equal



  • @DocShoe Ja? Ich würde das auch nicht static machen, bei mir sähe das wohl so aus:

    friend void swap(T& lhs, T& rhs)
    {
       using std::swap;
       swap(lhs.mData, rhs.mData);
       swap(lhs.mName, rhs.mName);
       swap(lhs.mStartValue, rhs.mStartValue);
    }
    

    Stackoverflow dazu



  • Hab´s aus Scott Meyers Effektiv C++ programmieren, Dritte Ausgabe, Tipp 25, Seite 131ff



  • @SeppJ
    Danke für die Informationen.

    Das Stichwort "Ressourcenklasse" half mir. Rule of Three bzw. Rule of Five bezieht sich nur auf Ressourcenklassen. Und das hatte ich früher offenbar manchmal nicht beachtet.

    Nach aktuellem Stand habe ich nur zwei Ressourcenklassen.

    Eine Resourcen Klasse verwaltet Windows Ressourcen. Da will ich keine Zuweisungsoperatoren usw.

    Die andere Ressourcen Klasse namens Point von mir speichert einen Punktnamen in Form eines C Strings. Algorithmisch bedingt wird diese Klasse sehr oft instanziiert (bis zu eine Million Instanzen sind üblich), aber nur sehr wenige Punkte (max. 4 Prozent) besitzen einen Namen.

    Ein std::string wäre zwar reizvoll, aber für den Großteil aller Instanzen würde unnütz Speicher allokiert werden. Und damit hat der Visual Studio Debugger ein Problem. Die Zeit das Programm zu beenden verlängert sich vermutlich wegen des Debug Heaps um bis zu 40 Sekunden.

    Ich bräuchte im Endeffekt einen String, welche wie std::string funktioniert aber standardmäßig solange keinen Speicher allokiert, bis ihm etwas zugewiesen wird.

    Edit:
    Tippfehler korrigiert


  • Mod

    @Quiche-Lorraine sagte in Copy And Swap Implementation ok?:

    Ein std::string wäre zwar reizvoll, aber für den Großteil aller Instanzen würde unnütz Speicher allokiert werden. Und damit hat der Visual Studio Debugger ein Problem. Die Zeit das Programm zu beenden verlängert sich vermutlich wegen des Debug Heaps um bis zu 40 Sekunden.

    Du machst allerlei Annahmen darüber, wie std::string funktioniert, die nicht stimmen. Oder du benutzt eine sehr alte Implementierung.
    Deine ganze Aussage macht keinen Sinn. Entweder allokiert deine Punktnamenklasse selber Speicher (wie es ein std::string unter Umständen auch tut) und du hast durch Selbermachen nur Nachteile gegenüber std::string. Oder du hast da ein statisches Array drin und deine Klasse ist kein Fall von Ressourcenverwaltung.



  • @DocShoe sagte in Copy And Swap Implementation ok?:

    Hab´s aus Scott Meyers Effektiv C++ programmieren, Dritte Ausgabe, Tipp 25, Seite 131ff

    Ich hab nur "Effective modern c++" hier, da steht von Copy & Swap leider nichts drin. Aber die friend Deklaration sorgt dafür, dass die Klasse "swappable" ist und Standard Algos den nutzen können

    Many standard library functions (for example, many algorithms) expect their arguments to satisfy Swappable, which means that any time the standard library performs a swap, it uses the equivalent of using std::swap; swap(t, u);.

    Typical implementations either

    1. Define a non-member swap in the enclosing namespace, which may forward to a member swap if access to non-public data members is required
    2. Define a friend function in-class (this approach hides the class-specific swap from name lookup other than ADL)

    (https://en.cppreference.com/w/cpp/named_req/Swappable)

    Ich persönlich bin einfach kein Freund davon, selbst etwas in namespace std{} zu schreiben und rechne auch nicht damit, dass jemand anderes das machen würde.
    Und mit

    using std::swap;
    swap(U, T);
    

    habe ich über ADL sichergestellt, dass, wenn es eine bekannt Spezialisierung gibt, die nehme und sonst std::swap genommen wird (wenn möglich). Das ist auch genau das, was die std Algos machen, daher reicht die friend Deklaration, dafür dass die eben den richtigen nehmen.



  • @Quiche-Lorraine sagte in Copy And Swap Implementation ok?:

    Ich bräuchte im Endeffekt einen String welche wir std::string funktioniert aber standardmäßig solange keinen Speicher allokiert bis ihm etwas zugewiesen wird.

    Ich kenne keine std::string Implementierung die (in Release Builds) für einen leeren String dynamisch Speicher anfordert. Was du siehst könnte das Iterator-Debugging von der Visual Studio Standard Library sein. Das führt IIRC in Debug Builds dazu dass für jeden Container oder String eine dynamische Speicheranforderung nötig ist. Der erzeugt da dynamisch irgend ein kleines Objekt das er zum Tracking der Iteratoren benötigt.

    Wenn du Iterator-Debugging auch in Debug Builds deaktivieren willst, kannst du einfach in deinen Projekteinstellungen das Präprozessormakro _ITERATOR_DEBUG_LEVEL auf 0 definieren.

    Was allerdings weiterhin bleibt ist dass ein std::string grösser ist als eine spezialisierte Klasse die nur einen einzigen Zeiger enthält. Nämlich grob 4 mal so gross (leicht unterschiedlich je nach Implementierung). Also z.B. 32 Byte statt 8 Byte. Bei 1 Million Strings wären das von der Grössenordnung her 20-30 MB, was auf modernen Systemen vermutlich wörscht ist. Dafür ist für sehr kurze Strings (z.B. 6 oder 8 Zeichen) typischerweise gar keine dynamische Speicheranforderung nötig.



  • @Schlangenmensch sagte in Copy And Swap Implementation ok?:

    Ich persönlich bin einfach kein Freund davon, selbst etwas in namespace std{} zu schreiben und rechne auch nicht damit, dass jemand anderes das machen würde.

    Bei std::swap ist das eigentlich recht üblich soweit ich weiss und war vom Standard bis inklusive C++17 explizit erlaubt. Wurde bei C++20 anscheinend geändert, keine Ahnung warum 😞

    Bei Standard-Library Klassen ist es weiterhin explizit erlaubt. Und bei Dingen wie std::hash kommst du auch gar nicht drumrum.



  • @hustbaer
    Danke, mit _ITERATOR_DEBUG_LEVEL 0 funktioniert es auch mit einem std::string!


Log in to reply