Objeke referenzen gebrauch, wie object copy vermeiden?



  • Hallo,

    ich bin verwirrt was Anlegen und Kopieren von Objekten betrifft:

    1. Ich lege eine Anzahl objekte an (verschachtelt)
    2. ich lass die objekte ausgeben
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    
    class A {
    
    
    	public:
    	
    		A(std::string _n) : n(_n) {
    			cout << "create: " << n << endl;
    		}
    		
    		~A() {
    			cout << "delete: " << n << endl;
    		}
    
    
    		std::string getN() {
    			return n;
    		}		
    		
    	private:
    	
    		std::string n;
    
    
    };
    
    class O {
    	
    	public:
    		O(std::string _n, A & _a) : n(_n), a(_a) {
    			cout << "create :" << n << endl;
    		}
    		
    		string getN() {
    			return n;
    		}
    	
    	private:
    	
    		std::string n;
    		A a;
    	
    };
    
    class E {
    	
    	public:
    	
    		E(O & _o) : o(_o) {
    			cout << "create e" << endl;
    		}
    		
    		~E() {
    			cout << "delete e" << endl;
    		}
    		
    		O getO() {
    			return o;
    		}
    		
    	private:
    	
    		O o;
    	
    };
    
    class S {
    
      public:
      
         void add_e(O & o) {
    		E * e = new E(o);
            e_v.push_back(e);
         }
         
         void list_as() {
    		 for (unsigned int i=0; i < e_v.size(); i++) {
    			 E * new_e = e_v[i];
    			 cout << new_e->getO().getN() << endl;
    		 }
    	 }
         
    	private:
    		vector<E *> e_v;
        
    
    };
    
    
    int main() {
    
      cout << "hello" << endl;
      
      A a0("abc");
      A a1("def");
      A a2("hij");
      
      O o0("123", a0);
      O o1("456", a0);
      
      E e0(o0);
      
      S s;
      
      s.add_e(o0);
      s.add_e(o1);
     
      s.list_as();
      
    }
    

    warum gibt es bei der ausgabe "create" und "delete" ausgaben? wie vermeidet man die?

    hello
    create: abc
    create: def
    create: hij
    create :123
    create :456
    create e
    create e
    create e
    123
    delete: abc  <<<<<<<<<<<<<<
    456
    delete: abc   <<<<<<<<<<<<<<<
    delete e
    delete: abc
    delete: abc
    delete: abc
    delete: hij
    delete: def
    delete: abc
    


  • @patrickm123

    warum gibt es bei der ausgabe "create" und "delete" ausgaben?

    Warum nicht?

    wie vermeidet man die?

    Man gibt nichts aus?



  • Du definierst ein paar Variablen von Klassentypen in der main, deren Konstruktoren und Destruktoren sollten doch nicht weiter überraschend sein. Gibt es irgendeine dieser vielen Ausgaben, die du wirklich nicht verstehst?



  • @Bashar danke, dass objekte beim anlegen (schritt 1) und beim programmende die constructors/destructors aufrufen verstehe ich
    meine frage wäre gibt es bei der ausgabe

    s.list_as();
    

    einen weg keine temp variablen zu gebrauchen, wo die constructors/destructors geskipped werden? die objekte existieren ja schon - wieso gibt es nochmal create/delete ausgaben?



  • In S::list_as, im Ausdruck new_e->getO().getN() rufst du E::getO auf:

    O getO() {
      return o;
    }
    

    Das erfordert eine Kopie von o. Da du keinen Kopierkonstruktor definiert hast, gibt es dafür auch keine Ausgabe, erst wenn die Kopie wieder zerstört wird im Destruktor. Wenn du keine Kopie willst, kannst du auch eine konstante Referenz auf o zurückgeben:

    const O& getO() { return o; }
    


  • Ein häufiger Fehler, den man am Anfang macht.
    Wenn du Textausgaben machen willst um zu prüfen, was wie wann wo erzeugt und zerstört wird, was ich vorbildlich finde, dann musst du aber auch alle Konstruktoren mit "Textausgaben" versehen.

    Es gibt noch den "Copy-Constructor"

    A( const A & )
    {
    }
    

    und den Move-Constructor

    A( A && )
    {
    }
    

    Wenn du verhindern willst, dass kopiert wird, kannst du auch all diese Operatoren und Konstruktoren entfernen.

    A( const A & ) = delete;
    A( A && ) = delete;
    A &operator=( const A & ) = delete;
    A &operator=( A && ) = delete;
    

    Du wirst aber feststellen, dass dein Quellcode dann nicht mehr kompiliert 😉
    Das "deleten" ist eine gute Methode, wenn man sicher sein will, dass Objekte nicht kopiert werden.



  • @Bashar , @It0101 sagte in Objeke referenzen gebrauch, wie object copy vermeiden?:
    Vielen Dank - euer feedback hilf meiner Lernkurve um einiges. Ich versuche nun:

    A( const A & )
    {
    cout << "copy a" << endl;
    }

    Damit sehe ich dass die 50% meiner Objekte hierüber angelegt werden. In der Ausgabe bekomme ich immer noch "dekete" und "copy" ausgaben:

    hello
    part 1: setup objects
    create: a0
    create: a1
    copy a
    create: o1
    copy a
    create: o2
    copy a
    create: o3
    copy a
    create e o1
    copy a
    create e o2
    ---- part 2: list objects
    copy a
    o1
    delete:
    copy a
    o2
    delete:
    delete:
    delete:
    delete:
    delete: a1
    delete: a0
    

    Da die Ojbekte ja im Speicher sind (durch Schritt 1: Anlegen der Objekte), müssten wir doch im Schritt 2 copy/delete von temp memory vermeiden können?

    Unten das aktuelle Programm:

    #include <iostream>
    #include <vector>
    #include <string>
    
    using namespace std;
    
    
    class A {
    
    
    	public:
    	
    		A(std::string _n) : n(_n) {
    			cout << "create: " << n << endl;
    		}
    		
    		A( const A & )
    		{
    			cout << "copy a" << endl;
    		}
    		
    		/*
    		A(A & _a) {
    			this->n = _a.n;
    		}
    		*/
    		
    		~A() {
    			cout << "delete: " << n << endl;
    		}
    
    
    		const std::string & getN() {
    			return n;
    		}		
    		
    	private:
    	
    		std::string n;
    
    
    };
    
    class O {
    	
    	public:
    		O(std::string _n, A & _a) : n(_n), a(_a) {
    			cout << "create: " << n << endl;
    		}
    		
    		/*
    		O(O & _o) {
    			this->n = _o.n;
    			this->a = _o.a;
    			cout << "clone : o" << endl;
    		}
    		*/
    		
    		const string & getN() {
    			return n;
    		}
    	
    	private:
    	
    		std::string n;
    		A a;
    	
    };
    
    class E {
    	
    	public:
    	
    		E(O & _o) : o(_o) {
    			cout << "create e " << o.getN() << endl;
    		}
    		
    		/*
    		E(E & _e) {
    			this->o = _e.o;
    			cout << "Clone e" << endl;
    		}
    		*/
    		
    		~E() {
    			cout << "delete e" << endl;
    		}
    		
    		const O & getO() {
    			return o;
    		}
    		
    	private:
    	
    		O o;
    	
    };
    
    class S {
    
      public:
      
         void add_e( O & o) {
    		E * e = new E(o);
            e_v.push_back(e);
         }
         
         void list_as() {
    		 for (unsigned int i=0; i < e_v.size(); i++) {
    			 // E * new_e = e_v[i];
    			 O o = e_v[i]->getO();
    			 cout << o.getN() << endl;
    		 }
    	 }
         
    	private:
    		vector<E *> e_v;
        
    
    };
    
    
    int main() {
    
      cout << "hello" << endl;
    
      cout << "part 1: setup objects" << endl;  
      A a0("a0");
      A a1("a1");
      
      O o0("o1", a0);
      O o1("o2", a0);
      O o2("o3", a1);
    
      
      S s;
      
      s.add_e(o0);
      s.add_e(o1);
     
      cout << "---- part 2: list objects" << endl;
      s.list_as();
      
    }
    
    
    


  • ok, verstehe, ich kann auch ein alias (const referenz) benutzen:

    	private:
    	
    		std::string n;
    		const A & a;
    

    damit fällt das kopieren weg 🙂



  • @patrickm123 sagte in Objeke referenzen gebrauch, wie object copy vermeiden?:

    O o = e_v[i]->getO();

    Auch wenn getO eine const ref zurückgibt, kopierst du hier in die Variable o. Du müsstest das const& mitschleppen, also const O& o = e_v[i]->getO();.

    Und was auch ein Problem bei deinem Code ist: du hast ein vector<E*> und erzeugst deine Kopien von Hand mit new. Dann musst du auch sicherstellen, dass du deine Objekte auch wieder mit delete löscht! (Oder einfach einen vector<E> nehmen oder std::vector<std::unique_ptr<E>> (oder vector<shared_ptr<E>>) benutzen)



  • Um mal noch zu ergänzen was WOB gesagt hat:

    Deine Klasse "S" hat keinen (selbst definierten) Konstruktor und Destruktor. Der Compiler baut dir hier automatisch jeweils einen von dieser Sorte. Der generierte Destruktor löscht zwar deinen Vector, aber da du im Vector Pointer hast, räumt der Vector quasi die Pointer weg ( 64bit ) aber nicht das Objekt auf das diese Pointer zeigen ( Größe: sizeof ( E ) ), denn dass musst du von Hand mit "delete" machen. Und daher musst du selbst einen Destruktor definieren.

    Anders wäre die Lage wenn du in dem Vector nicht E* sondern E speichern würdest, also die Objekt direkt im Vector. Dann würde der auto-generierte Destruktor ausreichen um sauber aufzuräumen.



  • @patrickm123 sagte in Objeke referenzen gebrauch, wie object copy vermeiden?:

    ok, verstehe, ich kann auch ein alias (const referenz) benutzen:

    	private:
    	
    		std::string n;
    		const A & a;
    

    damit fällt das kopieren weg 🙂

    Sind dir die Konsequenzen dieser Änderung bewusst?



  • @hustbaer guter punkt, d.h. änderungen in "a" sind nicht möglich über ein o Objekt. Allerdings wenn A jetzt viel Speicher brauchen würde, würde das Kopieren wegfallen?



  • @patrickm123 das andere, wichtigere Problem ist aber auch, dass nun das o-Objekt komplett von dem a-Objekt, mit dem es erzeugt wurde, abhängt. Das heißt, wenn jemand das a ändert, ist es in deinem o auch geändert. Wenn jemand das a-Objekt löscht, ist deine o-Objekt kaputt (die Referenz ist ungültig).



  • @It0101 danke für den Hinweis mit dem Aufräumen - die pointer variante hatte ich deshalb genommen, weil ich dynamisch Objekte in S anlegen wollte, ohne Objekte zu kopieren, es ist eher eine Lernübung. In einem "richtigen" Programm würde ich wohl Memory profiling machen müssen?



  • @wob danke, interessanter Punkt, dass mit dem Löschen wäre nicht so gut.... d.h. müsste man dann schauen wie man es z.B. über tests garantieren kann, dass das Programm ein gültiges a enthält



  • nein man müsste sein programmdesign so aufstellen, dass so etwas nicht vorkommen kann.......... 🤨



  • im destructor, sollte ich auf "pointer is not null" checken bevor ich delete aufrufe?



  • Nein, solltest du nicht. delete funktioniert auch auf Nullpointern.



  • @patrickm123 sagte in Objeke referenzen gebrauch, wie object copy vermeiden?:

    im destructor, sollte ich auf "pointer is not null" checken bevor ich delete aufrufe?

    Es darf nur nicht uninitialisiert sein bzw nicht "schon mal vorher deleted".



  • @patrickm123 sagte in Objeke referenzen gebrauch, wie object copy vermeiden?:

    @It0101 danke für den Hinweis mit dem Aufräumen - die pointer variante hatte ich deshalb genommen, weil ich dynamisch Objekte in S anlegen wollte, ohne Objekte zu kopieren, es ist eher eine Lernübung. In einem "richtigen" Programm würde ich wohl Memory profiling machen müssen?

    Du solltest nicht panische Angst vor dem Kopieren haben. Du musst dir bei deinem Design vor allem darüber klar sein, wem was gehört.
    Deswegen wäre es z.B. unüblich, ein Objekt auf dem Heap ( z.B. mit new erzeugt ), dass Klasse A gehört, einer anderen Klasse B als rohen Pointer zu übergeben, die mit Klasse A rein gar nichts zu tun hat. In dem Fall wäre eine Kopie durchaus in Ordnung. Oder ein alternatives Konzept wie "shared_ptr". Aber der Fokus sollte immer auf den Besitzverhältnissen liegen. Der Besitzer einer Ressource erzeugt sie und zerstört sie im Idealfall auch wieder. Und wenn die Ressource weitergeben wird ( aus A entfernt und nach B übergeben ), dann gibt es auch dafür wieder bessere Varianten als reine Pointer. Z.B. std::unique_ptr und/oder move-Semantik.

    Aus meiner Sicht ist die Nutzung von reinen Pointer eine absolute Notlösung wenn alle anderen Konzepte nicht tragbar sind.


Log in to reply