Dreierregel - wie am besten implementieren?



  • Wenn man innerhalb von Klassen mit Pointern arbeitet, muss man ja bei einer Kopie des Objekts eine tiefe Kopie erstellen. Da gibt es die zwei Möglichkeiten Copy-Konstruktor und operator=. So weit, so gut. Im Copy-Konstruktor kann ich ja in der Initialisierungsliste die Dinge kopieren, die 1:1 übernommen werden können und entsprechend im Body die Kopie der anderen Daten vornehmen, falls nötig. Nun gibt es aber wie gesagt noch den operator=. Auch für den muss ich die selbe Logik implementieren wie für den Copy-Konstruktor. Man nehme folgenden Fall als Beispiel:

    class foo
    {
        char *c;
    };
    
    foo f1, f2;
    
    f1 = f2;
    

    Hier würde die Adresse von f2.c einfach in f1.c kopiert werden, was aber unter Umständen nicht gewollt ist.

    Ich hab von jemandem gehört, dass man es mittels einer Swap-Methode machen soll. Dieses Beispiel hat er mir gegeben:

    struct foo
    {
       foo( foo const & f ) {}
       foo & operator=( foo const & f )
       {
          foo tmp( f );
          swap( tmp ); //<-- this calls foo::swap
    
          return *this;
       }
    
       void swap( foo & f )
       {
          // swap members here
       }
    };
    

    Zeigt natürlich nur die Struktur und nicht wie es letztendlich implementiert wird. Ich habe mir jetz mal eine kleine abstrakte Klasse geschrieben, von der ich Klassen ableite, die tiefe Kopien benötigen. So sieht sie aus:

    template <class T>
    class mxDeep
    {
        protected:
            bool bDeleteResources;
    
        public:
            mxDeep()
                :
                    bDeleteResources(true)
            {}
    
            virtual T &operator=(const T &copy) = 0;
            virtual void swap(const T &copy) = 0;
            virtual void cleanUp() = 0;
    
            void setResDelete(const bool bDelete)
            {
                this->bDeleteResources = bDelete;
            }
    };
    

    Ich leite dann eben davon ab und kann so nicht vergessen die benötigten Methoden zu implementieren.

    Ist meine Lösung gut oder gibt es eine bessere?



  • Max0r schrieb:

    Ist meine Lösung gut oder gibt es eine bessere?

    Sie ist nicht allgemeingültig, da man nicht bei jeder Klasse unbedingt eine swap-Methode braucht bzw. eine swap-Methode nicht immer sinnvoll umgesetzt werden kann.
    Aber andersrum ist bei Klassen wo es sowieso eine swap-Methode gibt das Copy&Swap-Idiom meist eine sinnvolle Umsetzung für den op=.



  • Ja, dass die nicht für jede Klasse gültig ist, ist mir klar. Deswegen hab ich ja dazugeschrieben, dass ich von ihr ableiten will, wenn ich eben tiefe Kopien brauch. Ich hab mich gefragt, ob man dann nicht auch im Copy-Konstruktor statt der Initialisierungsliste im Body swap() aufrufen könnte. Dadurch spart man sich die Redundanz, die man dadurch hat, dass man die Werte einmal in der Initialisierungsliste festlegt und einmal in der swap().



  • swap() im Kopierkonstruktor ist unsinnig. Mit welchem Objekt willst du da tauschen?



  • Nexus schrieb:

    swap() im Kopierkonstruktor ist unsinnig. Mit welchem Objekt willst du da tauschen?

    Naja nicht tauschen, sondern vom übergebenen Objekt in das aktuelle schreiben. Muss ja auch net swap sein, sondern evtl. ne Zwischenfunktion, die das übernimmt.

    // Edit

    Hab halt irgendwie an sowas hier gedacht:

    struct foo
    {
        int a, b;
        char *c;
    
        foo()
            :
                c(NULL)
        {}
    
        foo(const foo &f)
        {
            this->swap(f);
        }
    
        foo &operator=(const foo& f)
        {
            this->swap(f);
    
            return *this;
        }
    
        void swap(const foo &f)
        {
            if (this->c != NULL)
            {
                delete[] this->c;
            }
    
            unsigned int len = strlen(f.c);
    
            this->c = new char[len + 1];
    
            for (unsigned int i = 0; i < len; ++i)
            {
                this->c[i] = f.c[i];
            }
    
            this->c[len] = '\0';
    
            this->a = f.a;
            this->b = f.b;
        }
    };
    
    int main()
    {
        foo f1, f2;
    
        f1.a = 1;
        f1.b = 2;
        f1.c = "blabla";
    
        f2.a = 3;
        f2.b = 4;
        f2.c = "blabla2";
    
        f1 = f2;
    
        std::cin.get();
    }
    

    Nur fragt sich jetz, ob des ne gute Idee is.



  • Max0r schrieb:

    Muss ja auch net swap sein, sondern evtl. ne Zwischenfunktion, die das übernimmt.

    Genau. Und diese Funktion heisst Kopierkonstruktor. 😉

    Du musst eben jede Membervariable einzeln tauschen, wenn du ein exceptionsicheres Swap willst. Darin besteht ja der Trick: Es werden keine ganzen Objekte kopiert, sondern nur Teile davon getauscht. Da dieser Prozess bis auf BuiltIn-Typen rekursiv weitergeführt werden kann und Kopieren von BuiltIns keine Exceptions wirft, kann man Swap exceptionsicher machen.

    Mit deinem Code kommst du vom Copy&Swap weg und Exceptions werden wieder zum Problem. Wenn im Zuweisungsoperator die Speicheranforderung fehlschlägt, hast du einen inkonsistenten Objektstatus. Wie willst du ausserdem deine swap() -Funktion (wobei "swap" ein denkbar schlechter Name dafür ist) implementieren, wenn die Klasse grössere Member mit komplexer Kopiersemantik besitzen? Dann kannst du Zuweisungen auch nicht mehr sicher implementieren.

    Du solltest dir übrigens unbedingt mal RAII ansehen. STL-Container machen die Implementierung beispielsweise trivial, sicher und effizient.



  • Max0r schrieb:

    Wenn man innerhalb von Klassen mit Pointern arbeitet, muss man ja bei einer Kopie des Objekts eine tiefe Kopie erstellen.

    Nein. Muss man nicht. Kommt drauf an, was Du machen willst. Wenn ich einen Iterator kopiere, dann wird das, worauf der zeigt nicht kopiert.

    Max0r schrieb:

    Ist meine Lösung gut [...]?

    Nee, gefällt mir nicht. Viel zu kompliziert.



  • Ich glaub jetz hab ichs verstanden. Ich hab grade das und das gelesen. Mir war net klar, dass ja alles getauscht wird und dadurch der Speicher entsprechend freigeräumt wird (falls der Konstruktor richtig implementiert is). Werds mir wohl morgen nochmal anschauen. Danke...



  • @Max0r:
    ich möchte dich nur darauf hinweisen, dass du total auf dem holzweg bist (warst) 🙂 die einfachste variante wurde bereits gefunden, und ist nunmal copy & swap.
    ok, es kann immer noch was besseres geben wo bloss noch keiner draufgekommen ist, aber ich halte das in dem fall für sehr unwahrscheinlich.

    was "kann nicht vergessen zu implementieren" angeht: wenn man es vergessen kann, dann liegt der fehler einen etage tiefer.

    wenn es um zeiger geht, dann verwende ich smart-pointer.
    entweder shared_ptr oder scoped_ptr.
    scoped_ptr ist nicht kopierbar, d.h. man kann auch nicht vergessen den copy-ctor einer klasse zu implementieren die einen scoped_ptr enthält, also gibt es auch kein problem.
    destruktor muss man dadurch auch nicht implementieren.

    genauso ist es mit anderen dingen: wenn etwas (eine klasse) nicht kopiert werden darf, dann sollte sie auch non-copyable sein. wenn man das so macht, kann man diese klassen, genauso wie scoped_ptr, als pointer verwenden, und trotzdem nie etwas vergessen.

    ein hilfsding wie dein mxDeep ist also volkommen unnötig, und schlimmer noch: es versucht das problem an der völlig falschen stelle zu lösen.



  • Willkommen



  • hustbaer schrieb:

    was "kann nicht vergessen zu implementieren" angeht: wenn man es vergessen kann, dann liegt der fehler einen etage tiefer.

    genauso ist es mit anderen dingen: wenn etwas (eine klasse) nicht kopiert werden darf, dann sollte sie auch non-copyable sein. wenn man das so macht, kann man diese klassen, genauso wie scoped_ptr, als pointer verwenden, und trotzdem nie etwas vergessen.

    ein hilfsding wie dein mxDeep ist also volkommen unnötig, und schlimmer noch: es versucht das problem an der völlig falschen stelle zu lösen.

    Ich kann dir net ganz folgen. Meine Klasse soll kopierbar sein und dazu muss ich dann copy & swap implementieren, so wie ichs verstanden hab. Wie würdest du jetz sicherstellen, dass operator=, Copy-Ctor und Destruktor implementiert sind (dass du es nicht vergisst), wenn du nicht von einer abstrakten Klasse ableitest, die dich dazu zwingt (ohne shared_ptr und scoped_ptr zu verwenden)?



  • Du sollst ja eben gerade sowas scoped_ptr verwenden.



  • Max0r schrieb:

    Ich kann dir net ganz folgen. Meine Klasse soll kopierbar sein und dazu muss ich dann copy & swap implementieren, so wie ichs verstanden hab.

    Musst du nicht zwingend, sondern nur, wenn die vom Compiler generierten Funktionen (vor allem Zuweisungsoperator) nicht exceptionsicher sind. Wenn du aber Objekte als Member hast, welche RAII unterstützen (z.B. STL-Container oder Smart-Pointer), tun die automatisch das Richtige. Aber abgesehen davon kann eine swap() -Funktion auch für andere Dinge nützlich sein, weil sie sehr performantes Tauschen erlaubt.

    Max0r schrieb:

    Wie würdest du jetz sicherstellen, dass operator=, Copy-Ctor und Destruktor implementiert sind (dass du es nicht vergisst) [...]

    Daran musst du selbst denken, wenn du eine spezielle Kopiersemantik möchtest. Aber das Vergessen ist nicht wirklich ein Problem, sobald du dir der Thematik ein wenig bewusst bist. Versuche einfach, nicht manuell rumzufrickeln wie bei deinem char* -Beispiel. Nimm stattdessen vorgefertigte Lösungen aus der Standardbibliothek (und evtl. Boost), hier z.B. std::string .


Anmelden zum Antworten