Forum: PC-Programmierung shared_ptr wrapping einer Klassenhierachie in C++


von Bernd (Gast)


Lesenswert?

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

von B. S. (bestucki)


Lesenswert?

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
von Bernd (Gast)


Lesenswert?

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

von B. S. (bestucki)


Lesenswert?

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.

von B. S. (bestucki)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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

von tictactoe (Gast)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Jay (Gast)


Lesenswert?

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.

von Oliver S. (oliverso)


Lesenswert?

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

von Tom (Gast)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Tom (Gast)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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

von Tom (Gast)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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
Noch kein Account? Hier anmelden.