Deadly Diamond of Death - Teil 2

Das Diamantenproblem von letztem Mal ist leider nicht durch die virtuelle Vererbung einfach so zu lösen. Auch wenn nur noch ein Subobjekt der gemeinsamen Basisklasse in der Objekthierarchie vorhanden ist, kann es durch unterschiedliche Überschreibungen von virtuellen Funktionen zu Problemen kommen. Ein sehr ähnliches Problem entsteht auch dann, wenn es keine gemeinsame Basisklasse gibt, sondern zufällig zwei Funktionen aus unterschiedlichen Basisklassen die gleiche Signatur aufweisen.

Video

Code

Variante des Diamantenproblems

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
 
class Base
{
public:
  virtual void sayHello() const = 0;
};
 
class Left : virtual public Base
{
public:
  void sayHello() const
  {
    std::cout << "Hello from the left\n";
  }
};
 
class Right : virtual public Base
{
public:
  void sayHello() const
  {
    std::cout << "Hello from the right\n";
  }
};
 
class Join : public Left, public Right
{
public:
  void sayHello() const
  {
    std::cout << "Hello from Join\n";
  }
};
 
int main()
{
  Join j;
  j.sayHello();
}

Kollision aufgrund zufällig gleicher Signatur

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
 
class Left
{
public:
  virtual void sayHello() const
  {
    std::cout << "Hello from Left\n";
  }
};
 
class Right
{
public:
  virtual void sayHello() const
  {
    std::cout << "Hello from Right\n";
  }
};
 
class Join : public Left, public Right
{
public:
  void sayHello() const
  {
    std::cout << "Override\n";
  }
};
 
void leftSayer(Left const &l)
{
  l.sayHello();
}
 
void rightSayer(Right const &r)
{
  r.sayHello();
}
 
int main()
{
  Join j;
  j.sayHello();
  leftSayer(j);
  rightSayer(j);
}

Erklärung

Beide Listings lösen das gleiche Problem aus: Funktionen gleichen Namens und gleicher Signatur kollidieren in der Klasse Join, welche sie aus verschiedenen Teilen der Vererbungshierarchie zusammenführt. Die Ursache unterscheidet sich allerdings leicht: im Falle des Diamantenproblems liegt die Ursache in der gemeinsamen Basisklasse begraben. Dort wird die Funktion sayHello() aus der Klasse Base in den Klassen Left und Right überschrieben. Das ist in dem Fall notwendig, wenn die entsprechenden Klassen auch instanziiert werden sollen. Im zweiten Fall ist die Ursache etwas anders: die Funktionen in den Basisklassen Left und Right heißen nur zufällig gleich und haben die gleiche Signatur. Das kann einem bei der Verwendung von Mehrfachvererbung immer mal wieder passieren. Man mischt in einer eigenen Klasse zwei Basisklassen aus unterschiedlichen Bibliotheken und schon passiert eine derartige Kollision.

Die Lösung ist für beide Probleme die gleiche: Die Klasse Join muss die betreffende Funktion überschreiben und damit eindeutig festlegen, welche Implementierung zu verwenden ist. Der Compiler muss im Hintergrund leicht unterschiedliche Dinge tun: im Falle des ersten Listings schreibt er den Eintrag für sayHello() in die vtable der Klasse Base, aus der die Funktion ja ursprünglich kommt. Im zweiten Listing hat er aus den unterschiedlichen Basisklassen zwei vtables, in denen es jeweils einen Eintrag für sayHello() gibt. Damit müssen wir uns zum Glück nicht rumschlagen: der Compiler trägt völlig korrekt in beide vtables und neue Implementierung ein, so dass sich die Objekte auch bei Zugriff über die Referenzen auf die jeweiligen Basisklassen wie gewohnt verhalten.

Ein Wort noch zu dem zweiten Fall: man sollte immer genau prüfen, ob beide Basisklassen mit den kollidierenden Funktionen auch wirklich das gleiche Konzept ausdrücken. Ein (etwas konstruiertes Beispiel): wir haben eine Bibliothek für grafische Bedienelemente und die Standard Template Library von C++. Nun kommen wir auf die Idee, ein Textfeld implementieren zu wollen, welches man gleichzeitig als std::string verwenden kann (in dem Fall wird einfach der aktuelle Inhalt des Textfeldes als String verwendet). Unser naiver Ansatz, einfach von (der gedachten) TextField-Klasse und std::string parallel abzuleiten scheitert an length(). Beide Klassen bieten diese Funktion an: TextField als grafische Länge des Textfeldes in der Anzeige und std::string als Länge des Strings in Buchstaben. Hier eine gemeinsame Überschreibung für beide Funktionen anzugeben ist nicht sinnvoll möglich: die Konzepte “Länge in Pixel” und “Länge in Buchstaben” sind einfach zu unterschiedlich, als dass man sie verheiraten sollte. In dem Fall muss also auf die Mehrfachvererbung verzichtet werden und der Entwurf sollte nochmal einen langen, kritischen Blick ertragen müssen.