Proxys und const-correctness
-
Hi,
jetzt bin ich mit meinem Design in eine Sackgasse gelaufen. Ich habe ein Interface, von dem sowohl eine eigentliche Klasse als auch ein Proxy darauf erbt.
Der Proxy kennt die eigentliche Klasse und leitet Aufrufe einfach weiter, protokolliert vorher aber noch ein wenig o.ä.
Natürlich soll es vom Proxy aber eine const und eine non-const-Variante geben. Das erreicht man aber nicht durch ein
const Proxy p(eigentlichesObjekt);
gegenüberProxy p(eigentlichesObjekt);
, weil ja das Objekt im ctor entsprechend einmal non-const und einmal const sein müsste.Ähnlich wie beim Iterator wollte ich jetzt also eine Klasse ConstProxy machen, wovon eine Klasse Proxy erbt. So weit, so gut.
Jetzt stelle ich aber fest: ConstProxy überlädt natürlich die non-const-Methoden nicht, schließlich funktionieren die ja nicht. Jetzt kann ich aber ConstProxy nicht instanziieren, weil die Klasse abstrakt ist, weil die Methoden fehlen.
Hier etwas Code (nicht getestet), der das Problem illustriert:
class Interface { public: void someMethod() = 0; void someOtherMethod() const = 0; }; class ConstProxy : public Interface { public: ConstProxy(const Interface& actualObject) : actualObject(&actualObject) { } // void someMethod() = 0; die ja nicht ! void someOtherMethod() const { // mache irgendwas actualObject->someOtherMethod(); } private: const Interface* actualObject; }; class Proxy : public ConstProxy { public: Proxy(Interface& actualObject) : ConstProxy(actualObject), actualObject(&actualObject) { } void someMethod() // ja, hier { // mache irgendwas actualObject->someMethod(); } private: Interface* actualObject; }; class ActualObject : public Interface { // ... wie auch immer implementiert }; int main() { const ActualObject someObject; ConstProxy proxy(someObject); // FEHLER: someMethod nicht implementiert! }
Was tu ich jetzt? Proxy ist eigentlich das perfekte Pattern für meinen Anwendungsfall, aber const-correctness scheint damit unvereinbar. Oder habe ich etwas übersehen?
Triviale Lösung ist Methoden leer implementieren... Ganz sauber erscheint mir dann so etwas wie ConstInterface und Interface, wobei letzteres wieder von ersterem erbt. Aber irgendwie wird das alles wurschtelig, geht das nicht eleganter?
Vielen Dank und beste Grüße!
-
Da ist viel komisch. Erstmal speicherst du den Zeiger doppelt (Proxy & ConstProxy). Zweitens: Es ist tatsächlich ein Gewurschtel, dass du "echte" Konstanz (mittels const) und logische Konstanz (Mittels einer Extra-Klasse mit
Const
-Präfix) mischst.
const-correctness scheint damit unvereinbar.
Ich bastle gerade an einer Template-Lösung für Proxy herum.
-
Klar habe ich das doppelt gespeichert, einmal in konstanter und nicht-konstanter Variante, was spricht denn bitte dagegen? Die eine Klasse hat eben konstanten Zugriff, die andere nicht. Da man beide nutzen können sollte und ich keine doppelte Schreibarbeit will, ist das in meinen Augen elegant gelöst.
Und ich habe die Analogie ja schon gebracht: Man hat bei einem Iterator auch einen const_iterator und einen iterator. Das ist hier ähnlich, weil ich eben eine Indirektion auf das eigentliche Objekt habe; diesen Punkt betreffend unterscheidet sich mein Proxy also nicht von einem Iterator. Der "mischt" das auch, operator*() liefert bei const_iterator const value_type zurück und bei iterator value_type bzw. ist im einen Fall value_type definiert als const T oder wie auch immer, du verstehst, worauf ich hinauswill.
Normalerweise hat man eben Interface* und const Interface*, je nachdem, ob der Benutzer irgendeiner (dort unbekannten) Implementierung eben konstant darauf zugreifen möchte oder nicht. Das funktioniert aber bei einem Proxy nicht, eben weil im ctor das Objekt bei const Proxy auch const sein müsste und das Attribut der Klasse eben auch. Da fällt mir aber kein Sprachmittel ein, um das zu automatisieren... Außerdem könnte man das sowieso umgehen mit:
const Proxy prox1(...); Proxy prox2(prox1);
und schon hätte man nicht-konstanten Zugriff auf das verwiesene Objekte.
-
Vorschlag:
(Bitte nicht ausrasten wenn ich irgendwas überhaupt nicht berücksichtigt habe)class Interface { public: virtual void someMethod() = 0; virtual void someOtherMethod() const = 0; }; template<typename ValueT, typename Pointer = ValueT*, typename Reference = ValueT&> class ProxyBase : public Interface // Edit: Vergessen abzuleiten. { public: using value_type = ValueT; using pointer = Pointer; using reference = Reference; ProxyBase(reference actualObject) : actualObject(&actualObject) {} void someMethod() // ja, hier { // mache irgendwas actualObject->someMethod(); } void someOtherMethod() const // ja, hier { // mache irgendwas actualObject->someOtherMethod(); } private: pointer actualObject; }; using Proxy = ProxyBase<Interface>; using ConstProxy = ProxyBase<Interface const> const; #include <iostream> class ActualObject : public Interface { void someMethod() { std::cout << "ActualObject::someMethod()\n"; } void someOtherMethod() const { std::cout << "ActualObject::someOtherMethod() const\n"; } }; int main() { const ActualObject someObject; ActualObject someNonConstObject; ConstProxy proxy(someObject); proxy.someOtherMethod(); Proxy proxy2(someNonConstObject); proxy2.someOtherMethod(); proxy2.someMethod(); }
Edit: Moment, das klappt so nicht. :grübel:
Klar habe ich das doppelt gespeichert, einmal in konstanter und nicht-konstanter Variante, was spricht denn bitte dagegen? Die eine Klasse hat eben konstanten Zugriff, die andere nicht.
Autsch, übersehen. Tut mir Leid.
Ich fände es schöner, hätteConstProxy
gleich einen Zeiger auf non-const, würde diesen aber nur als Zeiger aufconst
behandeln.
-
Ja, das wäre perfekt!
Wieso klappt es denn nicht?Leider komme ich auf die Weise auch zu keinem Ergebnis. Irgendwie habe ich das Gefühl, C++ ist hier nicht flexibel genug... Die Vererbungsvariante erscheint zunächst logisch, löst aber eben nicht das Problem.
Die Sache ist ja auch: Selbst wenn ich es irgendwie hinkriege ein instanzierbares ConstProxy zu erstellen, so hat dieses Methoden, die im Interface drin sind. Und dann kriegt man es auch nicht hin, dass es einen CompileTime-Error gibt, wenn man die aufrufen möchte. Schließlich ist eine Instanz ConstProxy selbst ja nicht (notwendigerweise) const. Schön wäre, wenn der Fehler, der bei Deinem Code kommt (dass er bei bei konstantem Interface dessen nicht-const-Methode nicht aufrufen kann) nur bei Aufruf der jeweiligen Methode kommt.
Aber kann man wohl schlecht so hinbiegen? Man könnte enable_if nutzen, um die Methode zu killen oder den Rumpf zu leeren (irgendwie vll), aber das wäre auch nicht besser als über Vererbung die non-const-Methoden einfach mit leerem Rumpf auszustatten.
-
Eisflamme schrieb:
Ja, das wäre perfekt! Wieso klappt es denn nicht?
Weil ich
void someMethod() // ja, hier { // mache irgendwas actualObject->someMethod(); }
So deklariere, dass bei
ConstProxy
ja auf eine non-const Methode vonInterface
zugreife - über einenconst
-Zeiger.
Es geht ganz leicht, aber hässlich zu lösen:#include <type_traits> template<typename ValueT, typename Pointer = ValueT*, typename Reference = ValueT&> class ProxyBase : public Interface { using internal_pointer = typename std::remove_const<typename std::remove_pointer<Pointer>::type>::type*; public: using value_type = ValueT; using reference = Reference; ProxyBase(reference actualObject) : actualObject( const_cast<internal_pointer>(&actualObject) ) {} void someMethod() { actualObject->someMethod(); } void someOtherMethod() const { actualObject->someOtherMethod(); } private: internal_pointer actualObject; }; using Proxy = ProxyBase<Interface>; using ConstProxy = ProxyBase<Interface const> const;
Laut Standard wahrscheinlich erlaubt, sehe ich gleich mal nach.
Eisflamme schrieb:
Schließlich ist eine Instanz ConstProxy selbst ja nicht (notwendigerweise) const.
Doch, bei mir schon.
Und dann kriegt man es auch nicht hin, dass es einen CompileTime-Error gibt, wenn man die aufrufen möchte.
Doch! Siehe oben.
-
Ist auch deswegen Mist, weil ich dann über einen non-const ConstProxy tatsächlich die non-const-Methode aufrufen und sogar nutzen kann! ConstProxy darf ruhig non-const sein, denn man darf das Bezugsobjekt ändern (und mutable ist semantisch nicht korrekt, der Proxy ist ja tatsächlich nicht konstant, sein Zustand entspricht dem Objekt, auf das er verweist, finde ich).
-
Antwort statt Edit deines alten Posts bei Bezug auf meinen neuen wäre übersichtlicher btw.
Also konstanter Proxy und konstantes Bezugsobjekt sind für mich zwei verschiedene paar Schuhe, siehe Analogie zu const_iterator und iterator.
-
Also konstanter Proxy und konstantes Bezugsobjekt sind für mich zwei verschiedene paar Schuhe, siehe Analogie zu const_iterator und iterator.
Was geht an
using const Proxy = Interface*; using const Proxy = Interface const*;
nicht, abgesehen davon, dass es dir nicht genug Java-like ist?
-
also ohne das const in using const
typedef Interface* Proxy; typedef Interface const* ConstProxy;
-
Problem nicht Mal ansatzweise verstanden/versucht zu verstehen... Lies nochmal in Ruhe meinen OP und am Besten auch, was ein Proxy ist (Proxys nutzt man auch in C++, aber das scheinst Du nicht zu kapieren
)
-
Eisflamme schrieb:
Problem nicht Mal ansatzweise verstanden/versucht zu verstehen... Lies nochmal in Ruhe meinen OP und am Besten auch, was ein Proxy ist (Proxys nutzt man auch in C++, aber das scheinst Du nicht zu kapieren
)
Das einzige was nicht geht, ist das Methoden wrappen, genau.
Also bauen wir das nach und erhalten die Semantik der Pointer.
struct Proxy { Proxy(Interface* i) : i(i) {} Interface* i; void someMethod() { /*log*/ i->someMethod(); } void someOtherMethod() { /*log*/ i->someOtherMethod(); } }; struct ConstProxy { Interface const* i; ConstProxy(Interface const* i) : i(i) {} ConstProxy(Proxy p) : i(p.i) {} void someOtherMethod() { /*log*/ i->someOtherMethod(); } };
Proxys werden in C++ praktisch immer ohne Vererbung verwendet.
-
Und dein Problem
class Interface { public: void someMethod() = 0; void someOtherMethod() const = 0; }; class ConstProxy : public Interface
widerspricht OOP.
Das kann entweder Java-like durch
class ConstInterface { public: void someOtherMethod() const = 0; }; class Interface : ConstInterface { public: void someOtherMethod() const = 0; }; struct Proxy : Interface {...} struct ConstProxy : ConstInterface {...}
gelöst werden, oder eben mit Templates.
-
Mal davon abgesehen, dass das mit den typedefs nichts mehr zu hat: Aha, also den ganzen Code doppelt schreiben? Tolle Lösung... iterator erbt in manchen Implementierungen auch von const_iterator, um Mal die Analogie weiterzuverwenden. Das ist dann auch javalike für Dich?
Das Interface darüber brauche ich an anderer, hier im Thread nicht erwähnter Stelle. Ist ja schön, dass Du einfach sagst "das ist javalike und daher kacke", aber trotzdem ist das einfach eine Anforderung an die Lösung, die ich hier suche. Wenn Du das ignorierst, hilfst Du mir leider nicht.
Ich erläutere es trotzdem Mal: An anderer Stelle brauche ich das, weil dort entweder ein Proxy oder ein eigentliches Objekt übergeben wird und deren Methoden benutzt werden sollen. Ohne das Interface darüber muss die andere Klasse entweder zum Template werden oder ich brauche fixe Methoden für jede Art von Proxy. Dass zweiteres Käse ist, sollte klar sein. Und aus der Klasse ein Template machen, geht auch nicht, weil die Unterscheidung, ob Proxy oder Nicht-Proxy zur Laufzeit getroffen wird. Kurz: Ich brauche dynamische Polymorphie für meinen Proxy und daher das Interface.
Oder willst Du mir jetzt weißmachen, dass man dynamische Polymorphie ohne Vererbung umsetzen kann?
widerspricht OOP.
Erläuterung?
-
Vorschlag ohne Code-Duplikation:
struct Proxy : Interface{ Proxy(Interface* i) : i(i) {} Interface* i; void someMethod() { /*log*/ i->someMethod(); } void someOtherMethod() const { /*log*/ i->someOtherMethod(); } }; class ConstProxy { Proxy p; public: ConstProxy(Interface const* i) : p(const_cast<Interface*>(i)) {} void someOtherMethod() const { p->someOtherMethod(); } operator const Interface& () { return *i; } // imitiert "extends const Interface" };
Und ja, dein Vorhaben ist etwas widersprüchlich.
Also konstanter Proxy und konstantes Bezugsobjekt sind für mich zwei
verschiedene paar Schuhe, siehe Analogie zu const_iterator und iterator.class ConstProxy : public Interface // (1) class Proxy : public Interface (2)
(1): ConstProxy ist kein Interface, weil jedes Interface someMethod() implementiert haben muss. LSP und so.
(2) Proxy hat hier zwei Ebenen von const, die auf eine abgebildet wurde. Es ist nicht möglich, einen Proxy zu erstellen, der immer auf das gleiche Objekt zeigt, aber trotzdem someMethod aurufen kann. Hier bitte Zuständigkeiten trennen.
const iterator
kann auch das Objekt, auf das er verweist, verändern.
-
Wo ist bei der Kompositionsvariante der Vorteil ggü. Vererbung? Ja, ich halte mir die Vererbungshierarchie frei, aber die benötige ich doch sonst auch nicht. Imo dasselbe in grün.
Zu den Punkten (1) und (2) von Dir: Da hast Du Recht.
Wie löse ich denn jetzt, dass ich ConstProxy und Proxy haben möchte, aber ein Interface drüber sein soll? Ich könnte halt einfach auf die const-correctness verzichten bzw. nur non-const Access anbieten. Irgendwie muss ich hier immer zwischen diversen Übeln wählen.
-
class ConstProxy { Proxy p; public: ConstProxy(Interface const* i) : p(const_cast<Interface*>(i)) {} void someOtherMethod() const { p->someOtherMethod(); } operator const Interface& () { return *i; } // imitiert "extends const Interface" };
Diese Lösung ist immer noch falsch. Denn die Imitation ist immer noch keine echte Vererbung.
Ich würde Sagen, Eisflamme, nimm einfach die Lösung aus dem OP und implementiere die
someMethod
-Funktion einfach beiConstProxy
als ...Die Sache ist ja auch: Selbst wenn ich es irgendwie hinkriege ein instanzierbares ConstProxy zu erstellen, so hat dieses Methoden, die im Interface drin sind. Und dann kriegt man es auch nicht hin, dass es einen CompileTime-Error gibt, wenn man die aufrufen möchte.
private
.(1): ConstProxy ist kein Interface, weil jedes Interface someMethod() implementiert haben muss. LSP und so.
Also ist
Interface const
auch kein Interface?
Das Argument passt hier nicht, dennConstProxy
wrappt nur einen kleineren Teil, der, der für konstante Interface-Instanzen überhaupt erst nutzbar ist.
Das ist so gewollt.
Überhaupt funktioniert Vererbung nicht immer auf Basis des LSP.
-
Sone schrieb:
class ConstProxy { Proxy p; public: ConstProxy(Interface const* i) : p(const_cast<Interface*>(i)) {} void someOtherMethod() const { p->someOtherMethod(); } operator const Interface& () { return *i; } // imitiert "extends const Interface" };
Diese Lösung ist immer noch falsch. Denn die Imitation ist immer noch keine echte Vererbung.
Zeig einen Fall, wo das nicht geht.
(1): ConstProxy ist kein Interface, weil jedes Interface someMethod() implementiert haben muss. LSP und so.
Also ist
Interface const
auch kein Interface?Nein, aber Interface ist ein Interface const.
Überhaupt funktioniert Vererbung nicht immer auf Basis des LSP.
Doch, ausser sie ist private.
-
Oder protected. Und nein, auch wenn sie public ist, kann man in C++ damit Späße treiben wie man lustig ist.
Zeig einen Fall, wo das nicht geht.
void foo( Interface& i ); foo( meinconstproxy );
Hier wird nämlich nix protokolliert oder sonstwas. Also voll daneben.
-
Sone schrieb:
Zeig einen Fall, wo das nicht geht.
void foo( Interface& i ); foo( meinconstproxy );
Hier wird nämlich nix protokolliert oder sonstwas. Also voll daneben.