Proxys und const-correctness



  • Hi,

    ich habe mir Deine Lösung jetzt nochmal genauer angeschaut und muss sagen: die ist genial.

    Ich wäre nie darauf gekommen das so zu lösen, das ist äußerst geschickt. Wie kommt man auf so was? Das scheint jedenfalls zu erfüllen, was ich gerne haben möchte, es erscheint nur eben wenig intuitiv, dass man einen ConstProxy, der nicht brav erbt, als Argument für ein Interface angeben kann. Aber als Objekt, das nur sicherstellt, dass die Initialisierung über ein konstantes Objekt erfolgt, ist es dann doch wieder irgendwie intuitiv.

    Man muss jedoch auch noch operator const Interface* überladen, nicht jeder mag Referenzen. Aber sonst scheint das okay zu sein.

    Dankeschön



  • Find die Idee auch interessant. Weiß nicht, ob ich das jemals brauche, werd ich mir aber für alle Fälle merken.



  • Kurze Rückfrage:

    Die Lösung impliziert jetzt, dass const Proxy/ConstProxy das als const qualifiziert, was bezogen auf das referenzierte (eigentliche!) Objekt const ist, richtig?

    Wenn ich also den Verweis ändere, sodass der Proxy auf ein anderes Objekt "zeigt", dann ist das so gesehen auch eine const Aktion. Und dann sollte der Zeiger doch eigentlich mutable sein, genau so wie ein Setter für den Verweis dann const ist.

    Wenn ich hier nicht durch mutable trenne, kann ich den Verweis auf das Objekt eben nicht mehr ändern, was ja z.B. beim const_iterator auch geht. Wobei ich hier auch nochmal rückfragen möchte, was

    const iterator kann auch das Objekt, auf das er verweist, verändern.

    zu bedeuten hat. Das ist doch eben der Witz, dass das nicht geht?



  • Eisflamme schrieb:

    Die Lösung impliziert jetzt, dass const Proxy/ConstProxy das als const qualifiziert, was bezogen auf das referenzierte (eigentliche!) Objekt const ist, richtig?

    Richtig, const Proxy und ConstProxy sind das gleiche.

    Das muss aber so sein, weil dein Interface das so erfordert.

    Wenn ich also den Verweis ändere, sodass der Proxy auf ein anderes Objekt "zeigt", dann ist das so gesehen auch eine const Aktion. Und dann sollte der Zeiger doch eigentlich mutable sein, genau so wie ein Setter für den Verweis dann const ist.

    Du vermischst die Ebenen, wie ich schon mal gesagt habe.

    Es gibt

    iterator
    const iterator
    const_iterator
    const const_iterator
    

    Der Proxy ist das, auf das der Iterator zeigt. *iterator -> Proxy, *const_iterator -> ConstProxy. Der Proxy ist unmittelbar mit dem Interface verbunden. Sobald das Interface selbst State hat, geht das nicht mehr.

    struct Interface {
      int data; // hier expliziter State, denkbar wäre auch impliziter
    };
    

    Wenn dein Proxy den Pointer ändert, ändert sich das Verhalten, nicht aber data. Zudem können Invarianten verletzt werden (z.B.: Wenn einmal Interface.empty()==true, dann soll immer gelten Interface.empty()==true).

    Mag sein, dass das bei dir nicht zutrifft, aber ich finde das den naheliegendsten Weg. Der Pointer darf nicht verändert werden. Das wird durch die Vererbung erzwungen.

    Ein Proxy, der den Pointer ändert, hätte auch Probleme mit dem const. Ich sehe keine Lösung, wie das mit const correctness gelöst werden könnte.

    Daher würde ich eine zweite Ebene der Indirektion vorschlagen.

    typedef Proxy* ProxyPtr;
    typedef ConstProxy* ConstProxyPtr; // hier ev. einen schlanken Wrapper um implizit in Interface const* konvertiert werden zu können
    

    Jetzt hast du

    ProxyPtr // pointer/interface veränderbar
    const ProxyPtr // nur interface veränderbar
    ConstProxyPtr // nur pointer veränderbar
    const ConstProxyPtr // nichts veränderbar
    


  • Also was ich nach dem Lesen deines Posts als Problem meiner mutable-Lösung ansehe, ist dass ein const Proxy trotzdem sein Bezugsobjekt ändern kann, was ja nicht korrekt ist.

    Bei der Version mit der zweiten Indirektionsebene verstehe ich nicht, wie ich den Pointer verändern kann. Ich nutze den Proxy ja nicht so, dass ich sagen kann:

    ProxyPtr p;
    p = &actualObject1;
    // jetzt will ich den Zeiger ändern
    p = &actualObject2;
    

    oder wie hast Du Dir das syntaktisch gedacht?



  • Kleine Anmerkung:

    Eisflamme schrieb:

    ConstProxy(const Interface& actualObject) 
            : actualObject(&actualObject)
    

    Vorsicht sei hier geboten, wenn sowas ähnliches tatsächlich in deinem Code vorkommt. Wenn ich deinen Konstruktor mit einem temp. Objekt aufrufe

    ConstProxy cp(ActualObject());
    

    dann lebt das temporäre Objekt solange, wie die Referenz darauf. Dh, dein Zeiger verweist mitunter auf einen möglicherweise toten Bereich. Ergo, nehme niemals die Adresse von const& Variablen.



  • Ergo, nehme niemals die Adresse von const& Variablen.

    Man sollte einfach nie davon ausgehen, dass eine const-Referenz langlebig ist. Und das führt hier dazu, dass der komplette Code so nicht funktioniert, sondern man einen Zeiger nehmen muss - sowohl für die interne Referenzierung als auch für die gesamte Schnittstelle (um Inkonsistenz zu vermeiden).



  • Hm, ich weiß nicht... An der Semantik des Proxys wird doch klar, dass man dort keine temporären Objekte reingeben darf. Oder nimmt irgendwer an, dass der Proxy eine Kopie des Parameters erstellt? So funktionieren die aber nicht.

    Klar, syntaktisch merkt man bei einem Zeigerargument noch eher, dass es eine "Referenz" ist, die langlebig sein soll, aber eigentlich ist das mehr Interpretation als wirklich hinter einem Zeiger (vs. eine Referenz) steckt, finde ich.



  • Eisflamme schrieb:

    Hm, ich weiß nicht... An der Semantik des Proxys wird doch klar, dass man dort keine temporären Objekte reingeben darf. Oder nimmt irgendwer an, dass der Proxy eine Kopie des Parameters erstellt? So funktionieren die aber nicht.

    Klar, syntaktisch merkt man bei einem Zeigerargument noch eher, dass es eine "Referenz" ist, die langlebig sein soll, aber eigentlich ist das mehr Interpretation als wirklich hinter einem Zeiger (vs. eine Referenz) steckt, finde ich.

    Ich habe mir nicht im Detail angeschaut, was nun die endgütlige Fassung von deinem Design ist, es war nur ein allgemeiner Hinweis auf ein potentielles Leck 🙂 Der Rest liegt in deinen Händen. Ich weiß ja nicht, ob dieses "Idiom" sonstwo in deinem Code auftaucht. Jedes potentielle Risiko verringert die Nachhaltigkeit der Software. Potentielle Risiken minimieren, Nachhaltigkeit maximieren.



  • Schon richtig, ja, werde ich beizeiten Mal überdenken. Bisher weiß ich halt immer, was mit dem Argument geschieht, daher passiert es mir nicht, dass dort temporäre Objekte übergeben werden.

    Umso länger ich drüber nachdenke, umso berechtigter finde ich den Einwand jedoch. Dankeschön 👍

    Edit:
    Noch offene Frage zur Übersicht:

    Eisflamme schrieb:

    Also was ich nach dem Lesen deines Posts als Problem meiner mutable-Lösung ansehe, ist dass ein const Proxy trotzdem sein Bezugsobjekt ändern kann, was ja nicht korrekt ist.

    Bei der Version mit der zweiten Indirektionsebene verstehe ich nicht, wie ich den Pointer verändern kann. Ich nutze den Proxy ja nicht so, dass ich sagen kann:

    ProxyPtr p;
    p = &actualObject1;
    // jetzt will ich den Zeiger ändern
    p = &actualObject2;
    

    oder wie hast Du Dir das syntaktisch gedacht?



  • Ähnlich wie beim Iterator wollte ich jetzt also eine Klasse ConstProxy machen, wovon eine Klasse Proxy erbt. So weit, so gut.

    Erbt denn const_iterator von iterator (bzw. andersrum)?



  • Habe schon einige Implementierungen gesehen, die das so machen, ja.



  • Nun, warum benutzt du dann nicht den gleichen Trick wie dort? Desweiteren gibt es auch noch const_cast, wenn du deinen ConstProxy zu einem Proxy verwandeln moechtest, d.h. du erbst einfach und fuegst non-const-Methoden hinzu und das referenzierte Objekt wird mittels const_cast gefuegig gemacht. Oder was ist mit: Einfach Proxy zu implementieren und ConstProxy davon erben lassen. ConstProxy reimplementiert alle non-const-Methoden dahingehend, dass sie alle eine runtime-Exception werfen. Bei entsprechenden Unit-Tests ist das so gut wie ein Fehler zur Compilezeit.

    Umso länger ich drüber nachdenke, umso berechtigter finde ich den Einwand jedoch.

    Denk bloss nich zu lange drueber nach. Ein Interface ohne Beschreibung (Spezifikation) ist nichts wert, auch wenn sprechende Namen verwendet wurden. Vor- und Nachbedingungen gehoeren in die Dokumentation. Wer sich an diese nicht haelt, hat Pech. Warum soll jede Fehlbenutzung im Code abgefagen werden?



  • Finde das Okay. Allein von der Semantik des Wortes proxy ist klar, dass die Lebenszeit des übergebenen Objektes länger sein muss als die des Proxies.



  • Lbenszeit des übergebenen Objektes länger sein muss

    Wollte gerade länger/gleich hinzufügen, dann ist mir aufgefallen dass letzeres gar nicht möglich ist. 🙂

    Nun, warum benutzt du dann nicht den gleichen Trick wie dort?

    Es gibt da ein Problem. Denn wie facepalm richtig kommentiert hat, sollte ein ConstProxy tatsächlich nicht zu einem non-const Interface konvertierbar sein. Somit ist facepalms Lösung tatsächlich die einzig richtige.



  • Gerade ohn compiler runtergetippt. Sollte funktionieren:

    template<class I>
    class ProxyBase
    {
    public:
        template<class> friend class ProxyBase;
    
        ProxyBase(I& actualObject)
        : actualObject(&actualObject){}
    
        //conversion ctor
        ProxyBase(ProxyBase<Interface> const& other):actualObject(other.actualObject);
    
        //conversion op=
        ProxyBase & operator=(ProxyBase<Interface> const& other){
            actualObject = other.actualObject);
            return *this;
        }
    
        void someMethod()
        {
            // mache irgendwas
            actualObject->someMethod();
        }
        void someOtherMethod() const
        {
             // mache irgendwas
             actualObject->someOtherMethod();
        }
    
    private:
        I* actualObject;
    };
    
    typedef ProxyBase<Interface> Proxy;
    typedef ProxyBase<Interface const> ConstProxy;
    

    //edit nochmal kleinen eleganten Trick eingebaut und damit an template<> gespart...



  • Mal meine erste (bzw. zweite) Lösung angeguckt? 🙂
    Deine ist übrigens falsch. Man kann über ein ConstProxy non-const Methoden aufrufen.
    Dazu sollte das schon nicht einmal kompilieren, siehe erste Seite, ich hatte dieselben Probleme.



  • ConstProxy tatsächlich nicht zu einem non-const Interface konvertierbar sein

    Nun, man kann nicht alles haben. Entweder ist der ConstProxy nicht zu einem Interface konvertierbar, dann darf er nicht erben. Oder er ist Erbe und muss non-const-Methoden reimplementieren, die dann eine Exception werfen. Waehle zwischen Compilezeit const und Laufzeitpolymorphie (Interface, Vererbung)! Ersteres ist C++-like, letzteres Java-like (und in Java gibt es kein const).

    In CLOS kann man before- und after-Methoden angeben. Leider ist C++ nicht Lisp. Pech. Selbst habe ich noch nie eine Proxyklasse erbend von einem Interface in C++ gebraucht, weil a) const und non-const problematisch ist b) Proxies nur temporaere Objekte bisher waren. Wenn du was loggen moechtest, dann tu es! Wenn nicht, dann lass es. Keep it simple. Fuer mich riecht es mehr und mehr nach overengineering.

    Ist wie mit Spielen: Write games, not engines! Das laesst sich auch allgemeiner formulieren: Write programs, not Frameworks!



  • Keine Ahnung, man muss halt aufpassen, wo man ein Interface drübersetzt und nicht. Bei Java sucht man nach Gründen das zu tun, man will es einfach überall und manch einer fühlt sich toll, wenn er so eine kreative Ader an den Tag legt, dass er es auch wirklich überall findet.

    In meinem Fall erscheint es einfach angebracht. Ich bin im Refactoring dessen, was schon perfekt funktioniert hatte, aber nicht so toll erweiterbar war und nicht so super änderbar. Ich schaffe mir durch so ein Interface einfach die Flexibilität, die ich praktisch erprobt immer gewollt habe. Daher stellt es sich als optimale Lösung für mich heraus. Der Schlechtredner (damit meine ich nicht eine Person, sondern wirklich die Personifizierung der auf zu wenigen Infos basierenden Kritik) des Hauses kann jetzt sagen, dass der restliche Teil meiner Software Murks ist, wenn ich an dieser Stelle ein Interface brauche. Nur ist mit solchen Aussagen irgendwas Produktives anzufangen...?

    Ich bin auch ein Gegner von Overengineering und kenne sowohl Leute als auch Software, die es übertreiben. Aber das andere Extrem ist einfach grundsätzlich jede Art von Interface zu verteufeln, weil es ja "java-like" sein könnte und daher grundsätzlich auch sein muss. 😉 Das ist vor allem ohnehin das absolute Totschlagargument, weil es in C++ ja immer eine bessere Möglichkeit gibt. Es ist auch egal, ob die wirklich praktische Vorteile hat, Hauptsache ist, dass das in Java auf gar keinen Fall üblich ist.

    knivil:
    Man kann nicht alles haben. Aber was hast Du gegen facepalms Lösung? Die ist nah an allem dran (auch wenn ich die zweite Indirektion noch nicht verstehe, aber auch ohne ist es schon Mal ganz chic).

    Bei den restlichen Argumenten und Vorschlägen habe ich den Eindruck, dass wir das schon auf den vorherigen Seiten durchgekaut haben, von daher fällt mir kein neuer Kommentar mehr dazu ein. Trotzdem natürlich herzlichen Dank! Wenn ich dazu komme, schaue ich es mir nochmal genauer an, um sicherzustellen, dass es wirklich nichts Neues ist.



  • Aber was hast Du gegen facepalms Lösung?

    Warum sollte Proxy von Interface erben, waehrend ConstProxy es nicht tut? Das ist inkonsistent. Deswegen wuerde ich Proxy ebenfalls nicht von Interface erben lassen. Ich hoffe du hast trotzdem verstanden, warum deine Anforderungen nicht vereinbar sind.


Anmelden zum Antworten