Forum: PC-Programmierung Polymorphes clone ist einfach, wie aber (polymorph) assignen?


von CppBert (Gast)


Lesenswert?

Polymorphes clone ist einfach, wie aber (polymorph) assignen?

Info:
-Das ist ein stark simplifiziertes Beispiel - minimal Konstruktor damit 
mal von was von "Aussen" reinkommt (normalerweise viel komplexer)
-Ich kann das nicht statisch realisieren weil ich Basis-Klassen-Zeiger 
von einer Factory bekommen
-die behavior-Methode ist einfach nur ein Aktion die ich per 
Basis-Klassen Zeiger aufrufen kann (normalerweise mehr davon und 
komplexer)
-Ich clone polymorph mit einem Basis-Klassen Zeiger
-Original und Clone sind vom selben Typ - aber eben nur als 
Basis-Klassen Zeiger greifbar
-In meinem Echt-Bespiel sind es noch schlimmere Komposits auf vielen 
verscheidenen Basis-Klassen Zeigern (Ideen von hier sind aber wohl 
anwendbar)
-Für die Hetzjäger unter euch - Nein!, es ist keine Hausaufgabe :)

Ziel: ich möchte auf den polymorphen clone verändern und wieder auf das 
original zurückschreiben

Ideen:
-dynamic-cast geht nicht weil ich nur die Basis-Zeiger habe
-Ich könnte mit dem Visitor Pattern arbeiten - aber ist das 
zielführend/schön

Hat jemand noch ein paar nette (typesafe, ohne wildes ge-caste usw.) 
Ideen?
1
#include <iostream>
2
3
struct Base
4
{
5
  int common = 0;
6
  
7
  Base(int common):common(common)
8
  {
9
  }
10
    
11
  // das "operative" Interface meiner Ableitungen
12
  virtual void behavior() = 0;
13
  
14
  // polymorph clonen
15
  virtual Base* clone() = 0;
16
  
17
  // nur als Debug-Hilfe
18
  virtual void info()
19
  {
20
     std::cout << "Base int common: " << common << std::endl;;
21
  }
22
};
23
24
struct TypeA: Base
25
{
26
  TypeA(int common, int value):Base(common),value(value){}
27
  
28
  int value = 0;
29
  virtual void behavior() 
30
  { 
31
    common += 2;
32
    value *= 2; 
33
  };
34
  virtual Base* clone() { return new TypeA(*this); }
35
36
  virtual void info() 
37
  {
38
    Base::info();
39
    std::cout << "TypeA int value: " << value << std::endl;;
40
  }
41
};
42
43
struct TypeB: Base
44
{
45
  TypeB(int common, double value):Base(common),value(value){}
46
47
  double value = 0.0;
48
  virtual void behavior() 
49
  { 
50
    common += 2;
51
    value *= 2.0; 
52
  };
53
  virtual Base* clone() { return new TypeB(*this); }
54
55
  virtual void info() 
56
  {
57
    Base::info();
58
    std::cout << "TypeB double value: " << value << std::endl;;
59
  }
60
};
61
62
void CloneAndReassigne(Base* original)
63
{
64
  std::cout << "CloneAndReassigne" << std::endl;
65
  Base* clone = original->clone();
66
  std::cout << "Original:" << std::endl;
67
  original->info();
68
  clone->behavior();
69
  std::cout << "Clone nach Aenderung:" << std::endl;
70
  clone->info();
71
  *original = *clone;
72
  std::cout << "Original nach Aenderung:" << std::endl;
73
  original->info();
74
}
75
76
int main()
77
{
78
  TypeA a(1, 2);
79
  TypeB b(2, 4);
80
  
81
  CloneAndReassigne(&a);
82
  CloneAndReassigne(&b);
83
}

Zum Online Kompilieren/Spielen: https://onlinegdb.com/HJkWlge14

Ausgabe:

Mit den Problemen die ich klarerweise habe

CloneAndReassigne
Original:
Base int common: 1
TypeA int value: 2
Clone nach Aenderung:
Base int common: 3
TypeA int value: 4
Original nach Aenderung:
Base int common: 3
TypeA int value: 2 <-- nicht Basis-Klassen-Member wurde nicht veraendert

CloneAndReassigne
Original:
Base int common: 2
TypeB double value: 4
Clone nach Aenderung:
Base int common: 4
TypeB double value: 8
Original nach Aenderung:
Base int common: 4
TypeB double value: 4 <-- nicht Basis-Klassen-Member wurde nicht 
veraendert

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


Lesenswert?

CppBert schrieb:
> Polymorphes clone ist einfach, wie aber (polymorph) assignen?
>
> Hat jemand noch ein paar nette (typesafe, ohne wildes ge-caste usw.)
> Ideen?

Verfrachte die polymorphen Typen einfach in Implementierungsklassen und 
lasse die Klassen, die das Interface zum Nutzer darstellen einfach nur 
noch aus einem Zeig auf die Basis der Implementierungsklassen zeigen. 
Wenn Du std::shared_ptr<> verwendest, bekommst Du die Zuweisungen frei 
Haus geliefert.

Du must nur darauf achten, dass Du evtl. deep copies machen musst, wenn 
Du ein Objekt änderst. Beispiel dafür findest Du z.B hier: 
:https://github.com/TorstenRobitzki/Sioux/blob/master/source/json/json.h

mfg Torsten

von Tom (Gast)


Lesenswert?

Die Basis-Klasse als Wert zu behandeln wie hier
1
*original = *clone;
 führt zu Slicing: https://en.wikipedia.org/wiki/Object_slicing
Minimalinvasive Idee: https://onlinegdb.com/Sy7XuZeJE

von CppBert (Gast)


Lesenswert?

Torsten R. schrieb:
> Verfrachte die polymorphen Typen einfach in Implementierungsklassen und
> lasse die Klassen, die das Interface zum Nutzer darstellen einfach nur
> noch aus einem Zeig auf die Basis der Implementierungsklassen zeigen.
> Wenn Du std::shared_ptr<> verwendest, bekommst Du die Zuweisungen frei
> Haus geliefert.

Hört sich gut an - leider kann ich deinen Ausführungen nicht folgen, 
auch dein Beispiel bringt mich gerade nicht weiter

Kannst du es bitte (noch) ein bisschen konkreter beschreiben?

von CppBert (Gast)


Lesenswert?

Tom schrieb:
> Die Basis-Klasse als Wert zu behandeln wie hier*original = *clone; führt
> zu Slicing: https://en.wikipedia.org/wiki/Object_slicing
> Minimalinvasive Idee: https://onlinegdb.com/Sy7XuZeJE

Zuerst dachte ich - Wow super Idee, aber es gibt leider andere 
Komponenten die auf das Original per Pointer verweisen

...und vielleicht muss ich in Zukunft auch nur die durch behavior() 
veränderten Teile ersetzen - also wäre ein gezieltes Assign wohl doch 
irgendwie besser, trotzdem super Idee

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


Lesenswert?

CppBert schrieb:
> Torsten R. schrieb:

> Kannst du es bitte (noch) ein bisschen konkreter beschreiben?

Hier Dein Beispiel:
1
#include <iostream>
2
#include <memory>
3
4
class Base
5
{
6
public:
7
  Base() = delete;
8
9
  void behavior()
10
  {
11
    impl_->behavior();
12
  }
13
14
  void info() const
15
  {
16
    impl_->info();
17
  }
18
19
protected:
20
  struct Impl {
21
    int common;
22
23
    explicit Impl(int c) : common(c) {}
24
25
    virtual void behavior() = 0;
26
    virtual void info() const {
27
      std::cout << "Base int common: " << common << std::endl;
28
    }
29
30
    virtual ~Impl() {}
31
  };
32
33
  explicit Base(const std::shared_ptr< Impl > p) : impl_(p) {}
34
35
  std::shared_ptr< Impl > impl_;
36
};
37
38
struct TypeA: Base
39
{
40
  TypeA(int common, int value) : Base( std::make_shared< ImplA >( common, value ) ) {}
41
42
private:
43
44
  struct ImplA : Impl {
45
46
    ImplA(int c, int v)
47
      : Impl( c )
48
      , value( v )
49
    {
50
    }
51
52
    void behavior() override
53
    {
54
      common += 2;
55
      value *= 2;
56
    }
57
58
    void info() const override
59
    {
60
      Base::Impl::info();
61
      std::cout << "TypeA int value: " << value << std::endl;;
62
    }
63
64
    int value;
65
  };
66
67
};
68
69
70
struct TypeB: Base
71
{
72
  TypeB(int common, double value) : Base( std::make_shared< ImplB >( common, value ) ) {}
73
74
private:
75
76
  struct ImplB : Impl {
77
78
    ImplB(int c, double v)
79
      : Impl( c )
80
      , value( v )
81
    {
82
    }
83
84
    void behavior() override
85
    {
86
      common += 2;
87
      value *= 2;
88
    };
89
90
    void info() const override
91
    {
92
      Base::Impl::info();
93
      std::cout << "TypeB double value: " << value << std::endl;;
94
    }
95
96
    double value;
97
  };
98
99
};
100
101
void CloneAndReassigne(Base& original)
102
{
103
  std::cout << "CloneAndReassigne" << std::endl;
104
  Base clone = original;
105
  std::cout << "Original:" << std::endl;
106
  original.info();
107
  clone.behavior();
108
  std::cout << "Clone nach Aenderung:" << std::endl;
109
  clone.info();
110
  original = clone;
111
  std::cout << "Original nach Aenderung:" << std::endl;
112
  original.info();
113
}
114
115
int main()
116
{
117
  TypeA a(1, 2);
118
  TypeB b(2, 4);
119
120
  CloneAndReassigne(a);
121
  CloneAndReassigne(b);
122
}

Das polymorphe Verhalten bekommst Du in den von Base::Impl abgeleiteten 
Klasse. Die von Base abgeleiteten Klassen fungieren eigentlich nur noch 
als factories. Das ganze hat dann value Semantik und polymorphes 
Verhalten, ohne dass Zeiger an der Schnittstelle genutzt werden müssen.

von CppBert (Gast)


Lesenswert?

@Torsten R.

Auf jeden Fall ein nettes Konzept - aber es hat mir viel zu starke 
Auswirkungen auf meine Gesamtarchitektur (Impl, Shared-Pointer, alle 
werden zu Value-Types), sonst aber interessant

Ich habe jetzt meine Clone Routine so veraendert das sie nicht einfach 
eine Kopie liefert sondern einen FeedbackClone - der eine virtuelle 
Feedback() anbietet die man dazu nutzen kann den Clone wieder in das 
Original "einzuprägen" - das Konzept ist leider immer noch nicht 
wirklich sauber von meinen "Arbeitsklassen" getrennt aber eine 
ausreichend kleine konzeptionell sehr lokale Lösung

Danke für eure Ideen - wie immer sehr Informativ hier

von tictactoe (Gast)


Lesenswert?

CppBert schrieb:
> -dynamic-cast geht nicht weil ich nur die Basis-Zeiger habe

CppBert schrieb:
> *original = *clone;

Man könnte das schon mit einem virtual copy-assignment machen:
1
class Base {
2
public:
3
   virtual Base& operator=(const Base& src) = 0;
4
...
5
};
6
7
struct TypeA: Base
8
{
9
   TypeA& operator=(const TypeA&) = default;
10
   TypeA& operator=(const Base& src) override   // Gimmick: covariant!
11
   {
12
      if (auto src1 = dynamic_cast<const TypeA*>(&src))
13
         *this = *src1;
14
      else
15
         Base::operator=(src);  // Achtung! Slicing!
16
      return *this;
17
   }
18
   ...
19
};
Alternativen:
- Auf die Kovarianz kannst du verzichten, wenn du sie nicht brauchst.
- Wenn du garantieren kannst, dass die Zuweisung zur Laufzeit nur mit 
kompatiblen Objekten geschieht, kannst du den dynamic_cast mit 
Referenzen machen und auf den else-Zweig verzichten (dann kommt eben 
eine Exception, sollten die Typen mal doch  nicht passen).

Gegen das Slicing im else-Zweig kann man nichts machen. Soll man 
wahrscheinlich aber auch nicht, denn was soll sonst die Bedeutung einer 
Zuweisung von X an Y sein, wenn deren Bestandteile nur teilweise 
übereinstimmen?

von tictactoe (Gast)


Lesenswert?

tictactoe schrieb:
> class Base
> {
> public:
>    virtual Base& operator=(const Base& src) = 0;
> ...
> };

Das sollte besser sein:
1
class Base 
2
{
3
public:
4
   virtual Base& operator=(const Base& src) = default;
5
...
6
};

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.