Hallo Leute, ich würde gerne eine größere Hierarchie von Klassen hinter shared_ptrs verstecken. Wenn es klappt, hätte es mehrere Vorteile: 1) polymorphe Pointer sollten praktisch "typfreies" C++ ermöglichen 2) die shared_ptrs nehmen mir das Speichermanagement ab 3) wenn man eine komplexe Library derartig hinter shared_ptrs versteckt, ist sie ähnlich einfach zu benutzen wie z.B. in Python Sagen wir, ich habe drei Klassen, erstens "Object", von dem alle anderen Klassen abgeleitet sind, zweitens "A", von "Object" abgeleitet, drittens "B", ebenfalls von "Object" abgeleitet. Nun würde ich gerne die Pointerklassen pObject von shared_ptr<Object> ableiten, pA von shared_ptr<A>, und pB von shared_ptr<B>, und nur noch Variablen des Typs "pObject" benutzen, z.B.: pObject a=pA(arg0,arg1); pObject b=pB(arg2,arg3); pObject c=a.someMethod(b); Leider funktioniert es nicht so einfach, denn die Hierarchie zwischen Object, A, B wird nicht auf pObject, pA, pB übertragen. Außerdem funktioniert Polymorphie nicht wie bei "raw" Pointern: wenn ich z.B. pA::someMethod(pObject o) und pA::someMethod(pB b) implementiere, und dann wie oben a.someMethod(b) aufrufe, wird pA::someMethod(pObject o) aufgerufen, obwohl *b zur Laufzeit den Typ *B enthält. Konnte ich das Problem verständlich machen? Habt ihr schon mal ähnliches versucht und habt Lösungsvorschläge oder Workarounds? Danke Bernd
Bernd schrieb: > Habt ihr schon mal ähnliches > versucht und habt Lösungsvorschläge oder Workarounds? Ich mach das meistens so:
1 | typedef std::shared_ptr<class base> ptr; |
2 | |
3 | class base{ |
4 | /* ... */
|
5 | };
|
6 | |
7 | class derived : |
8 | public base |
9 | {
|
10 | public:
|
11 | ptr Create(); |
12 | /* ... */
|
13 | protected:
|
14 | /* Konstruktoren */
|
15 | };
|
base ist hier ein reines Interface. Ansonsten kriegt base auch noch eine Create-Funktion. Create macht nichts anderes als einen Konstruktor aufzurufen. Bernd schrieb: > Außerdem funktioniert Polymorphie nicht wie bei "raw" Pointern: wenn ich > z.B. pA::someMethod(pObject o) und pA::someMethod(pB b) implementiere, > und dann wie oben a.someMethod(b) aufrufe, wird pA::someMethod(pObject > o) aufgerufen, obwohl *b zur Laufzeit den Typ *B enthält. Ich übergebe meistens Referenzen. Der Funktion soll es agal sein, wo und wie das Objekt existiert, Hauptsache es existiert. Bei Containern wird der Zeiger übergeben, aber der interessiert sich dann auch nicht für das Objekt, auf das der Zeiger zeigt. Eine Möglichkeit wäre auch, für den Zeiger eine eigene Klasse zu erstellen, die dann die Funktionsaufrufe entsprechend weiterleitet. Der Nachteil daran ist der erhöhte Schreibaufwand: Diese Klasse müsste alle Funktionen kennen, die auf das Objekt angewendet werden.
:
Bearbeitet durch User
Das eigentliche Problem scheinen überladene Funktionen zu sein. Wie löst du folgendes? Um bei deinem Beispiel zu bleiben, nehmen wir mal an, du hast eine überladene Funktion, die je nach Typ des Arguments unterschiedle Ergebnisse liefert: base doSomething(base){ ... return base(...);} base doSomething(derived){ ... return derived(...);} Wie sieht dies nach deiner Methode aus, wenn jedes base und derived in shared_ptrs versteckt ist? Da entstehen viele Probleme. doSomething(ptr) kann nicht mehr unterscheiden, ob es mit einem shared_ptr<base> oder einem shared_ptr<derived> aufgerufen wird. Bei der Rückgabe kannst du kein shared_ptr<derived> liefern, wenn shared_ptr<base> als Rückgabetyp deklariert ist, eben weil die Hierarchie nicht übertragen wird, wie ich oben schrieb. Selbstverständlich kann ich irgendwelche Klassen in shared_ptrs wrappen, aber dabei gehen Polymorphie und Vererbungshierarchie drauf. Bernd
Bernd schrieb: > Wie sieht dies nach deiner Methode aus, wenn jedes base und derived in > shared_ptrs versteckt ist? Genau so, wie wenn ich keine shared_ptr verwenden würde. Ich übergebe entweder Referenzen oder nackte Zeiger (nur wenn der Zustand "kein Objekt" existiert). Der Funktion soll es egal sein, ob das Objekt eine lokale oder globale Variable ist, von einem smart pointer oder einem nackten Zeiger gehalten wird. Bernd schrieb: > doSomething(ptr) kann nicht mehr unterscheiden, ob es mit einem > shared_ptr<base> oder einem shared_ptr<derived> aufgerufen wird. Das schon, denn dies sind zwei unterschiedliche Typen. Aber einen shared_ptr<derived> willst du gar nicht, du willst ja nur shared_ptr<base>. Die Funktion, der du den shared_ptr übergibst, muss halt dann die nächste Funktion aufrufen:
1 | base DoSomething(shared_ptr<base> Ptr){ |
2 | return DoSomething(*Ptr); |
3 | }
|
Ich halte das jedoch für unsinnig, da auch der Aufrufer einfach den Zeiger dereferenzieren kann.
Nachtrag: Bernd schrieb: > eben weil die > Hierarchie nicht übertragen wird, wie ich oben schrieb. shared_ptr, unique_ptr und weak_ptr sind halt auch nur Klassen wie alle anderen auch. Diese werden Zeiger genannt, weil sie sich aufgrund ihrer Memberfunktionen ähnlich wie Zeiger verhalten. Sie können einige Dinge, die nackte Zeiger nicht können, können aber auch nicht alles, was nackte Zeiger können. Smart Pointer sind primär dafür da, dass der Programmierer kein delete vergessen kann.
Bernd schrieb: > Konnte ich das Problem verständlich machen? Habt ihr schon mal ähnliches > versucht und habt Lösungsvorschläge oder Workarounds? Nicht so ganz, ich fürchte es ist etwas schwierig zu verstehen, da Du uns Deine Lösung presentierst und nicht das eigentliche Problem. Polymorphy ohne pointer ist relativ einfach, wenn Du in eine Klasse einen Zeiger auf eine polymorphe Implementierung nimmst und das Interface der Basisklasse über diese Klasse exportierst. Einfaches Beispiel, die möchtest Zahlen Filtern und ein Filter hat genau eine polymorphe Funktion, die verschiedenartige Zahlen filtert.
1 | class filter |
2 | { |
3 | public: |
4 | bool operator()(int) const; |
5 | protected: |
6 | struct impl |
7 | { |
8 | ~impl() {} |
9 | virtual bool filter_impl(int) const = 0; |
10 | }; |
11 | |
12 | explicit filter( impl* ); |
13 | |
14 | private: |
15 | |
16 | std::shared_ptr< impl > pimpl; |
17 | }; |
18 | |
19 | class and_filter : public filter { |
20 | public: |
21 | and_filter( const filter& lhs, const filter& rhs ); |
22 | }; |
23 | |
24 | class odd_numbers : public filter { |
25 | public: |
26 | odd_numbers(); |
27 | }; |
28 | |
29 | class even_numbers : public filter { |
30 | public: |
31 | even_numbers(); |
32 | }; |
33 | |
34 | // keine pointer aber immer noch polymorphes verhalten |
35 | static const filter no_numbers = and_filter( odd_numbers, even_numbers ); |
Die Polymorphy steckt in filter::impl. Die von filter abgeleiteten Klassen dienen eigentlich nur als Factories für verschiedene Implementierungen von filter::impl, die der Basisklasse durchgereicht werden. Ein etwas umfangreicheres Beispiel, bei dem ich dieses Muster mal verwendet habe, ist ein Teil eines Webservers, der JSON Daten implementiert: https://github.com/TorstenRobitzki/Sioux/blob/master/source/json/json.h Wenn es Dir aber einfach darum geht, dass es sich wie Python anfühlen soll, dann würde ich (Trommelwirbel) Python verwenden ;-) mfg & HTH Torsten
Torsten R. schrieb: > Die Polymorphy steckt in filter::impl. Die von filter abgeleiteten > Klassen dienen eigentlich nur als Factories für verschiedene > Implementierungen von filter::impl, die der Basisklasse durchgereicht > werden. Oder anders ausgedrückt: Du arbeitest auch nur mit shared_ptr<base>, nur hast du den hinter einer Klasse versteckt statt den Client-Code damit rumspielen zu lassen.
tictactoe schrieb: > Oder anders ausgedrückt: Du arbeitest auch nur mit shared_ptr<base>, nur > hast du den hinter einer Klasse versteckt statt den Client-Code damit > rumspielen zu lassen. Ja, der shared_ptr ist aber ein Implementierungsdetails. Man könnte auch mit raw pointern arbeiten oder das reference counting mit in der Basisklasse implementieren. Man hat vor allem nicht die Pointer-Syntax, wenn man damit arbeitet.
Wie war der Standard-Spruch? Smart pointers are smart but not pointers . Versuch nicht gegen die Sprache zu arbeiten. Das gibt nur mehr Ärger und Scherereien, besonders später bei der Wartung. Ein paar der Dinge die du machen möchtest lassen sich z.B. seit C++11 zum Teil mit auto erledigen. Wobei auto zwar nur Kosmetik ist, aber es geht ja ums Aussehen
1 | auto a = std::make_shared<A>(arg0, arg1); |
2 | auto b = std::make_shared<B>(arg2, arg3); |
3 | auto c = a.someMethod(b); |
Dabei sieht es so aus, als ob a, b, und c vom selben Typ sind, aber der Compiler setzt die eigentlichen Typen ein, es ist immer noch statische Typisierung, nicht dynamische. Das make_shared kannst du noch wie oben vorgeschlagen hinter Create Methoden verbergen, aber das ist auch nur Kosmetik.
Bernd schrieb: > Außerdem funktioniert Polymorphie nicht wie bei "raw" Pointern: wenn ich > z.B. pA::someMethod(pObject o) und pA::someMethod(pB b) implementiere, > und dann wie oben a.someMethod(b) aufrufe, wird pA::someMethod(pObject > o) aufgerufen, obwohl *b zur Laufzeit den Typ *B enthält. Das funktioniert allerdings auch mit "raw"-Pointern nicht. Oliver
Bernd schrieb: > Nun würde ich gerne die Pointerklassen pObject von shared_ptr<Object> > ableiten, pA von shared_ptr<A>, und pB von shared_ptr<B>, und nur noch > Variablen des Typs "pObject" benutzen, z.B.: > pObject a=pA(arg0,arg1); > pObject b=pB(arg2,arg3); > pObject c=a.someMethod(b); Wenn eine Funktion nur Argumente vom Typ shared_ptr<Object> bekommt, hat sie keine Möglichkeit, anhand der Signatur zu unterscheiden, ob die gespeicherten Pointer eigentlich ein A oder B sind. Es bleiben nur 2 Wege, die aber beide extrem ineffizient sind: 1) Du erweiterst deine Object Klasse um z.B. ein static int, das für jede abgeleitete Klasse eine andere id definiert, und um eine virtuelle Methode getId(), die diesen Wert liefert. Anhand dieser id baust du in deiner A::someMethod einen switch(id){...} ein, der die unterschieldichen Fälle abarbeitet. 2) Du verwendest ebenfalls einen switch oder if/else if/else if ..., benutzt aber RTTI, statt deine eigenen type ids mitzuschleppen. Beides ist aber derart ineffizient, dass du besser direkt in javascript oder Python programmieren solltest. Tom
Tom schrieb: > Wenn eine Funktion nur Argumente vom Typ shared_ptr<Object> bekommt, hat > sie keine Möglichkeit, anhand der Signatur zu unterscheiden, ob die > gespeicherten Pointer eigentlich ein A oder B sind. aber genau das ist doch der Sinn von Polymorphie: gleiches Aussehen bei unterschiedlichem Verhalten. Wenn A und B keine gemeinsames Interface haben, dann sollten Sie sich auch keine Basisklasse teilen.
Torsten R. schrieb: > aber genau das ist doch der Sinn von Polymorphie: gleiches Aussehen bei > unterschiedlichem Verhalten. Wenn A und B keine gemeinsames Interface > haben, dann sollten Sie sich auch keine Basisklasse teilen. Du vergleichst hier Äpfel und Birnen. Polymorphie bedeutet folgendes: > pObject a=pA(arg0,arg1); > pObject b=pB(arg2,arg3); > a->someMethod(b); Hier ruft a->someMethod tatsächlich eine Methode der Klasse A auf, obwohl a als shared_ptr<Object> definiert ist. DAS ist Polymorphie. Wenn du dann aber zwei Methoden fun(pObject) und fun(pA) definierst, wird nach obigen Zeilen ein Aufruf fun(a) trotzdem NICHT fun(pA) aufrufen, sondern fun(pObject), weil a den Typ pObject hat. Polymorphie funktioniert NICHT unter FunktionsARGUMENTEN. Oder hast du ein Beispiel, das das Gegenteil zeigt? Tom
Hi Tom, Tom schrieb: > Wenn du dann aber zwei Methoden > fun(pObject) und > fun(pA) definierst, wird nach obigen Zeilen ein Aufruf > fun(a) trotzdem NICHT fun(pA) aufrufen, sondern fun(pObject), weil a den > Typ pObject hat. Polymorphie funktioniert NICHT unter > FunktionsARGUMENTEN. richtig, aber ich muss den Punkt übersehen haben, bei dem der OP das gefordert hat :-| > Oder hast du ein Beispiel, das das Gegenteil zeigt? Naja, üblicherweise wäre fun() dann ein member von Object und in A und B entsprechend implementiert. Wenn Du verschiedenste (und viele) Funktionalitäten um eine übersichtliche Klassen-Hierarchie hast, dann finde ich das Visitor pattern im ganz elegant. mfg Torsten
Torsten R. schrieb: > richtig, aber ich muss den Punkt übersehen haben, bei dem der OP das > gefordert hat :-| Der OP will alle Variablen nur als Typ pObject speichern, schrieb er (vergleichbar mit dem keyword "var" in Javascript, denke ich). Ausserdem: Bernd schrieb: > wenn ich > z.B. pA::someMethod(pObject o) und pA::someMethod(pB b) implementiere, > und dann wie oben a.someMethod(b) aufrufe, wird pA::someMethod(pObject > o) aufgerufen, obwohl *b zur Laufzeit den Typ *B enthält. Er möchte anscheinend, dass eine Methode pA::someMethod(pObject arg0) zur Laufzeit unterscheidet, ob arg0 nun auf ein Object, auf ein A oder auf ein B zeigt, und demnach enflechtet, welche der urspünglichen someMethods gemeint ist, was nun einmal unmöglich ist. Warum du immer wieder dein Visitorpattern in den Raum wirfst, verstehe ich auch nicht. Tom
Tom schrieb: > Warum du immer wieder dein Visitorpattern in den Raum wirfst, verstehe > ich auch nicht. Ups, was tue ich?
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.