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
typedefstd::shared_ptr<classbase>ptr;
2
3
classbase{
4
/* ... */
5
};
6
7
classderived:
8
publicbase
9
{
10
public:
11
ptrCreate();
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.
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
baseDoSomething(shared_ptr<base>Ptr){
2
returnDoSomething(*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.
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
autoa=std::make_shared<A>(arg0,arg1);
2
autob=std::make_shared<B>(arg2,arg3);
3
autoc=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