copy constructor und return by value



  • Salut zusammen

    Ich hab eine zu einer Objektklasse gehörende Funktion, die mir ein Objekt einer anderen Klasse zurückgibt, als Beispiel:

    Apfel Apfelbaum::ernten()
    {
    Apfel rot;
    rot.Herkunft()=Apfelbaumlocation();
    
    return rot;
    

    Und würde jetzt gerne noch, aus was für einem Grund auch immer, gerne den Apfel über einen Pointer im Apfelbaum-Objekt registrieren.

    Das (temporäre) Problemchen:
    Der Apfel rot ist eine lokale Variable: Wenn ich dessen Adresse im Apfelbaum registriere ist das eigentlich wenig sinnvoll, da die Variable zerstört wird (werden sollte).

    Die (naheliegende) Lösung:
    Die Registrierung des Apfels erfolgt in dessen Copy Constructor, der aufgerufen wird (werden sollte), wenn die lokale Variable Apfel in einen anderen Scope kopiert wird, also bei

    Apfelbaum baumBeimHaus;
    Apfel frischVomBauer=baumbeimHaus.ernten();
    

    Das eigentlich Problem:
    g++ und wohl auch andere Compiler die mich jetzt allerdings nicht interessieren optimieren die lokale Kopie weg und lassen die Funktion ernten() direkt das Objekt frischVomBauer schreiben. Und damit fällt auch der Copyconstructor ins Wasser. (Mag zwar n'ziemlicher Performancegewinn sein, erstaunt mich aber, dass dies auch passiert wenn ein cctor explizit definiert wurde. )
    Jetzt könnte ich natürlich wieder in ernten() die lokale Variable registrieren, was funktioniert, da der Compiler bei der Optimierung dort schon das spätere Objekt verwendet das nicht zerstört wird, aber nicht sonderlich schön ist plus auch noch compilerabhängig, dazu verwirrend, und natürlich in c++ auch zu Warnungen führt.

    Was kann man tun? Weiss ich da was nicht? Kann ich den Aufruf des Copyconstructors erzwingen? (Ohne zuerst den Apfel mit dem Standardkonstruktor zu kreieren und dann einen operator= zu bemühen)
    Oder ist tatsächlich die einzige Möglichkeit, die Registrierung sowohl für die lokale Variable vorzunehmen (die dann mit dem Destruktor rückgängig gemacht wird falls vom compiler verwendet) als auch im copy constructor?

    Merci und Gruss



  • Oder ist tatsächlich die einzige Möglichkeit, die Registrierung sowohl für die lokale Variable vorzunehmen (die dann mit dem Destruktor rückgängig gemacht wird falls vom compiler verwendet) als auch im copy constructor?

    Ja und nein.

    Ich würde sagen du hast hier einen klassischen Designfehler.
    Du versuchst Objekte mit "Identität" mit einer Klasse zu implementieren, der du Value-Semantik verpasst (=kopierbar, zuweisbar). Das beisst sich einfach.

    Bzw. überleg dir mal folgendes: was soll passieren, wenn ich folgenden Code schreibe:

    Apfel a1 = baum.ernten();
    Apfel a2 = a1;
    Apfel a3 = a1;
    // ...
    

    Ich hab' jetzt drei "gleiche" Äpfel, der Apfelbaum wurde aber nur 1x "geerntet". Welche Äpfel/welcher Apfel soll nun im Baum registriert sein? Bzw. ist es überhaupt immer möglich Äpfel zu "kopieren"? Was wenn der Baum nur einen Apfel hatte, und nach Apfel a1 = baum.ernten(); bereits "leer" ist? Hab ich mir dann zwei Äpfel "erzaubert"? Und wieso kann es Äpfel geben die vom gleichen "Zweig" des Baums kommen (apfel.Herkunft())?

    ----

    Es gibt mehrere Möglichkeiten wie man das besser lösen kann. Allen gemeinsam ist, dass eine Klasse vorkommt die "noncopyable" ist.

    1. Du machst den Apfel "noncopyable", und gibtst nur einen Zeiger auf den Apfel zurück. Dabei kannst du entweder einen rohen Zeiger zurückgeben, oder einen Smart-Pointer. z.B. std::auto_ptr, boost::shared_ptr, boost::intrusive_ptr oder wenn du einen Compiler mit C++0x Support hast auch std::unique_ptr.

    1.1) Du kannst statt einem Apfel ein "ApfelHandle" von der Funktion zurückgeben lassen. Das "ApfelHandle" ist dabei vom Prinzip her auch nur eine Art Smart-Pointer. D.h. das "ApfelHandle" selbst ist kopierbar und zuweisbar und was nicht alles. Das "ApfelHandle" kann allerdings direkt Funktionen anbieten um den Apfel zu modifizieren bzw. Informationen vom Apfel abzufragen. Der Vorteil hier ist, dass das Interface des Apfelbaums nicht von einer speziellen Smart-Pointer Klasse abhängig ist. Zu Fuss programmieren muss man deswegen auch nichts, denn um das "ApfelHandle" zu implementieren kannst du ohne Bedenken eine fertige Smart-Pointer Klasse verwenden. Diese lässt sich dann bei Bedarf leicht austauschen, da Programmteile die mit dem Apfelbaum und seinen Äpfeln arbeiten keinen direkten Zugriff auf den intern verwendeten Smart-Pointer haben.
    (Der "noncopyable" Teil ist hier die eigentliche "Apfel" Klasse, auf die das "ApfelHandle" intern einen Zeiger hält. Auf die "Apfel" Objekte hat man bei dieser Variante dann typischerweise von Aussen keinen Zugriff.)

    1. Du machst den Apfel "noncopyable" aber "movable". Ohne Rvalue-Referenzen (C++0x Feature) geht das nur umständlich, mit ist es relativ einfach. "Movable" heisst du kannst den Apfel nur "kopieren", wenn das originale Objekt dabei zerstört wird.

    Wenn du eine dieser Möglichkeiten wählst, dann hast du also eine Klasse die "noncopyable" ist, und in deren Konstruktor kannst du die Objekte im Apfelbaum registrieren. Und im Destruktor de-registrieren. Und Copy-Konstruktor gibt es ja keinen, da die Klasse ja "noncopyable" ist.
    (OK, bei der "movable" Variante gibt es einen Move-Konstruktor und einen Move-Assignment-Operator, wo man auch Code brauchen wird der die Äpfel registriert/de-registriert).

    ----

    Achja: ob Äpfel nun eine "Identität" haben oder "Value-Semantik", ist eine Design-Entscheidung. Was Sinn macht wird sich von Fall zu Fall unterscheiden. Sobald du aber irgendwelche Objekte irgendwo "registrieren" willst, heisst das, dass die Objekte "Identität" haben.

    Falls es möglich ist, die Notwendigkeit zu entfernen die Äpfel im Apfelbaum zu registrieren, dann sehe ich keinen absoluten Grund, warum "Value-Apfel" schlecht sein müssen. Das wäre also auch ein gangbarer Weg.

    Kommt halt immer darauf an was deine Äpfel sind, und was du mit ihnen machen willst.



  • @hustbaer: Vielen Dank für diese ausführliche Antwort! Sie streift mein Problem leider nur am Rand, da das Ding mit den Äpfeln nur ein Anschauungsbeispiel ist. Dennoch sehr interessant. Ich nehme an, Objekte mit "Identität" kommt von irgend einem noch ausführlicheren theoretischen Postulat o.ä. her: Hast du dazu gerade nen Link oder Lektüre die du empfehlen würdest? Klingt sehr interessant - ansonsten google ich einfach mal drauflos 🙂

    Um dem groben von mir angedachten Anwendungsgebiet näher zu kommen und dein Beispiel aufzugreifen: Was sich bei mir auf die Äpfel bezog, könnte ich jetzt bei deinem Beispiel auf die Apfelhandles beziehen: Beispielsweise könnte ich mir wünschen, in einem Apfel alle auf ihn "zeigenden" Handles zu registrieren. So könnte ich dann, falls der Apfel (vielleicht in Folge eines Programmierfehlers) zerstört wird, aber noch Handles auf ihn bestehen, diese als ungültig kennzeichnen, sodass ich dann bei Verwendung derselbigen entsprechende Fehler ausgeben kann. Ein Anwendungsbeispiel wären da vielleicht auch Iteratoren auf die Äpfel im Apfelbaum 🙂
    Falls sich da sinnvollere Lösungsmöglichkeiten für das Problem anbieten, habe ich natürlich offene Ohren!

    Gruss



  • hustbaer schrieb:

    ...

    Ganz ehrlich hustbaer, du schreibst unglaublich gehaltreiche Posts, danke! Das mit "Identität" vs "Value-Semantik" hast du cool auf den Punkt gebracht, wurde mal Zeit dass das einer anspricht. Ist mE sehr wichtig zu wissen und ich hab's noch nie irgendwo angesprochen gesehen.



  • ratzefatz schrieb:

    Dennoch sehr interessant. Ich nehme an, Objekte mit "Identität" kommt von irgend einem noch ausführlicheren theoretischen Postulat o.ä. her: Hast du dazu gerade nen Link oder Lektüre die du empfehlen würdest? Klingt sehr interessant - ansonsten google ich einfach mal drauflos 🙂

    Google einfach mal drauf los, denn ich kann dir keine Quellen dazu nennen. Die Unterscheidung ist etwas was man beim Programmieren dauernd macht, und da ich keinen besseren Namen dafür weiss, nenne ich es "Identität". Im Prinzip geht es um Value-Semantik vs. Reference-Semantik, aber wenn man danach sucht findet man meist Erklärungen was Pointer bzw. Referenzen (in Sprachen wie Java/C# etc.) sind, und weniger wann man was verwenden soll.

    Um dem groben von mir angedachten Anwendungsgebiet näher zu kommen und dein Beispiel aufzugreifen: Was sich bei mir auf die Äpfel bezog, könnte ich jetzt bei deinem Beispiel auf die Apfelhandles beziehen: Beispielsweise könnte ich mir wünschen, in einem Apfel alle auf ihn "zeigenden" Handles zu registrieren. So könnte ich dann, falls der Apfel (vielleicht in Folge eines Programmierfehlers) zerstört wird, aber noch Handles auf ihn bestehen, diese als ungültig kennzeichnen, sodass ich dann bei Verwendung derselbigen entsprechende Fehler ausgeben kann.

    Naja, die einfachste Variante sowas zu lösen, ist die Äpfel so lange am Leben zu halten, so lange es noch Handles darauf gibt. Genau das machen shared_ptr und intrusive_ptr.

    Wenn es nicht sinnvoll ist, die Äpfel am Leben zu halten, so lange es noch Handles darauf gibt, sondern nur erkannt werden soll ob ein Handle noch "gültig" ist, könnte man sich eine darauf spezialisierte Smart-Pointer/Handle Klasse basteln.
    Wie man sowas am effizientesten umsetzt, hängt vom konkreten Anwendungsfall ab. Kann ja sein, dass man sowieso schon irgendwo Datenstrukturen pflegen muss, die man dazu verwenden kann bzw. entsprechend erweitern.

    Eine relativ einfach Möglichkeit wäre, sich inetwa das nachzubauen, was shared_ptr macht. Dazu könnte man intrusive_ptr verwenden, so dass man nicht viel selbst programmieren muss. Beispiel:

    #include <boost/noncopyable.hpp>
    #include <boost/intrusive_ptr.hpp>
    #include <memory>
    #include <stdexcept>
    #include <string>
    #include <iostream>
    
    class Apple : private boost::noncopyable
    {
    public:
    	explicit Apple(std::string const& color) : m_color(color)
    	{
    	}
    
    	std::string const& GetColor() const
    	{
    		return m_color;
    	}
    
    private:
    	std::string m_color;
    };
    
    struct AppleTrackingStub : private boost::noncopyable // entspricht inetwa dem "shared count" von boost::shared_ptr
    {
    	explicit AppleTrackingStub(Apple* apple) : m_references(0), m_apple(apple)
    	{
    	}
    
    	size_t m_references;
        Apple* m_apple;
    };
    
    void intrusive_ptr_add_ref(AppleTrackingStub* atd)
    {
        atd->m_references++;
    }
    
    void intrusive_ptr_release(AppleTrackingStub* atd)
    {
        atd->m_references--;
        if (atd->m_references == 0)
        {
            // Löschen des Apfels wenn das letzte Handle darauf verschwindet
            // (bzw. man muss den Apfel auch nicht löschen, kommt auf die Anwendung an,
    		//  vielleicht ruft man auch nur eine Funktion des "Apfelbaums" auf,
    		//  um den Apfel an den Baum zurückzugeben - was auch immer)
            delete atd->m_apple;
            atd->m_apple = 0;
    
            // Und die Tracking-Struktur muss natürlich auch weg
            delete atd;
        }
    }
    
    class AppleHandle // kopierbar, zuweisbar, default-konstruierbar und das alles
    {
    public:
    	explicit AppleHandle() // erstellt ein "leeres" (ungültiges) Apfel-Handle
    	{
    	}
    
    	static AppleHandle NewApple(std::string const& color) // erstellt ein Handle auf einen neuen Apfel
    	{
    		std::auto_ptr<Apple> appleGuard(new Apple(color));
    		AppleHandle handle;
    		handle.m_trackingStub.reset(new AppleTrackingStub(appleGuard.get()));
    		appleGuard.release(); // Ownership wurde an handle.m_trackingStub übergeben
    		return handle;
    	}
    
    	Apple& GetApple()
        {
    		if (!m_trackingStub)
                throw std::logic_error("cannot get apple: handle is invalid (empty).");
            else if (!m_trackingStub->m_apple)
                throw std::logic_error("cannot get apple: already deleted.");
    		else
                return *m_trackingStub->m_apple;
        }
    
        void DeleteApple()
        {
    		if (!m_trackingStub)
                throw std::logic_error("cannot delete apple: handle is invalid (empty).");
            else if (!m_trackingStub->m_apple)
                throw std::logic_error("cannot delete apple: already deleted.");
    		else
    		{
    			delete m_trackingStub->m_apple;
    			m_trackingStub->m_apple = 0;
    		}
        }
    
    private:
        boost::intrusive_ptr<AppleTrackingStub> m_trackingStub;
    };
    
    int main()
    {
    	AppleHandle handle;
    
    	try
    	{
    		std::cout << "apple color: " << handle.GetApple().GetColor() << "\n";
    	}
    	catch (std::exception const& e)
    	{
    		std::cout << "exception: " << e.what() << "\n";
    	}
    
    	handle = AppleHandle::NewApple("green");
    	std::cout << "apple created successfully\n";
    
    	try
    	{
    		std::cout << "apple color: " << handle.GetApple().GetColor() << "\n";
    	}
    	catch (std::exception const& e)
    	{
    		std::cout << "exception: " << e.what() << "\n";
    	}
    
    	try
    	{
    		handle.DeleteApple();
    		std::cout << "apple deleted successfully\n";
    	}
    	catch (std::exception const& e)
    	{
    		std::cout << "exception: " << e.what() << "\n";
    	}
    
    	try
    	{
    		std::cout << "apple color: " << handle.GetApple().GetColor() << "\n";
    	}
    	catch (std::exception const& e)
    	{
    		std::cout << "exception: " << e.what() << "\n";
    	}
    
    	try
    	{
    		handle.DeleteApple();
    		std::cout << "apple deleted successfully\n";
    	}
    	catch (std::exception const& e)
    	{
    		std::cout << "exception: " << e.what() << "\n";
    	}
    
    	return 0;
    }
    

    Wenn das ganze thread-safe sein soll, wird natürlich alles komplizierter (leider). In dem Fall müsste man sich ansehen welche Operationen genau thread-safe sein müssen, welche auf Geschwindigkeit optimiert werden sollten etc. (Wenn Geschwindigkeit kein wichtiger Faktor ist, dann könnte man sich mit boost::shared_ptr relativ einfach etwas basteln was thread-safe ist, dafür aber ein wenig langsamer als eine "optimale" Lösung)

    Eine andere Möglichkeit wäre alle Handles die auf den selben Apfel zeigen in einer Linked-List (Ring-Liste) zu verwalten. Dadurch spart man sich den "tracking stub", und Zugriffe auf den Apfel werden schneller (eine Indirektion weniger). Dafür wird das Kopieren/Zuweisen des Handles eine Spur langsamer, das Handle wird etwas grösser (+ 2 Zeiger), und das Löschen des Apfels wird langsamer (man muss alle Handles durchgehen und überall den Zeiger auf den Apfel auf NULL setzen).

    Mit Thread-Safety wird's bei der Linked-List Variante aber wirklich kompliziert. Bzw. wenn man es sich einfach macht, leidet die Performance vermutlich so schlimm, dass man besser wegkommt wenn man gleich die einfachere boost::shared_ptr Variante macht.

    ----

    Ich würde aber sagen dass es vermutlich besser ist, wenn du etwas mehr über deine konkrete Anwendung verrätst. Also was deine Äpfel sind und was der Apfelbaum etc.



  • hustbaer schrieb:

    Eine relativ einfach Möglichkeit wäre, sich inetwa das nachzubauen, was shared_ptr macht. Dazu könnte man intrusive_ptr verwenden, so dass man nicht viel selbst programmieren muss. Beispiel:(...)

    Hmm ja, das gefällt mir. Was würdest du von der davon abgeleiteten Variante halten, auf dem Heap ein von Apfel und Handles gemeinsam verwaltetes Objekt zu erstellen (noncopyable), das Auskunft gibt über die Validität des Apfels: Dabei würde das letzte Objekt aus dem Pool {Apfel,zugehörige Handles} dann das Objekt zerstören wennn der Destruktor aufgerufen wird. Der Apfel könnte dann ganz einfach dieses Objekt updaten wenn er zerstört wird und so alle Handles darüber benachrichtigen.

    Der Vorteil wäre, dass bei dieser Variante auch der Apfel direkt verwendet werden könnte und nicht "hinter den Handles verborgen" wäre.


Anmelden zum Antworten