Gleichnamige Member zweier Basisklassen

Es kann passieren, dass man von zwei Klassen erbt, die zwar nichts miteinander zu tun haben, aber dennoch gleichnamige Member haben. Stellen wir uns vor, dass eine benannte Liste erstellen wollen. Dabei soll sich die Liste wie eine Liste, aber auch wie ein String verhalten:

Die Problemstellung

class Node
{
};
 
class List
{
private:
  class Node * First;
  class Node * Last;
 
public:
  List();
 
  /* Funktionen zum Einfügen usw. werden für das Beispiel nicht benötigt */
 
  unsigned int GetLength();
};
 
class String
{
private:
  char Text[256];
 
public:
  String();
 
  /* Funktionen zum Bearbeiten des Strings werden für das Beispiel nicht benötigt */
 
  unsigned int GetLength();
};
 
 
class NamedList : public List
                , public String
{
};

Dabei ergibt sich folgendes Problem. Sowohl die Liste, wie auch der String besitzen die Methoden GetLength() - im einen Fall wird die Länge der Liste zurückgegeben, im anderen Fall die Länge des Strings. Das ganze funktioniert solange, bis man das erste Mal die Funktion GetLength() von einem NamedList-Objekt aufruft:

int main( void )
{
  NamedList myList;
 
  printf( "Length: %d\n", myList.GetLength() );
 
  return EXIT_SUCCESS;
}

Dies kann der Compiler nicht kompilieren (hier die Ausgabe des GCC-Compilers):

main.cpp: In function ‘int main()’:
main.cpp:10: error: request for member ‘GetLength’ is ambiguous
string.h:11: error: candidates are: unsigned int String::GetLength()
list.h:17: error:                 unsigned int List::GetLength()

Für die Funktion GetLength() gibt es nun zwei Kandidaten und der Compiler weiß nicht, welche der beiden Funktionen nun genommen werden soll. Das Beispielprojekt ist so geschrieben, dass die Beispiel-Liste immer den Wert 10 zurück und der String ist immer 42 Zeichen lang.

Lösung 1: Festlegen der zu rufenden Funktionen durch Konvertierung

Eine Namedlist ist eine Liste und eine Liste weiß, welche Methode sie rufen muss. Selbiges gilt für den String: eine NamedList ist ein String und ein String weiß ebenfalls genau welche Methode sie rufen muss. Man kann die NamedList also auf den gewünschten Datentyp zuweisen und von diesem aus die Methode GetLength() rufen.

#include <stdio.h>
#include <stdlib.h>
 
#include "namedlist.h"
 
int main( void )
{
  NamedList myList;
 
  List & list = myList;
  String & string = myList;
 
  printf( "Length String: %d\n", list.GetLength() );
  printf( "Length Liste: %d\n", string.GetLength() );
 
  return EXIT_SUCCESS;
}

Damit ist der Code wieder kompilierbar und liefert folgendes Ergebnis:

Length String: 42
Length Liste: 10

Dieses Verfahren ist auf den ersten Blick sehr ungewöhnlich, insbesondere, wenn man sich die einfachere 2. Lösung ansieht. Ich führe sie hier nicht auf, weil sie bevorzugt verwendet werden soll, sondern weil sie zeigt, dass sich die Frage, welche Methode verwendet werden muss in dem Moment geklärt hat, wo eine NamedList Instanz an eine Funktion übergeben wird, die eine String bzw. List-Referenz benötigten. Die jeweiligen Funktionen sind an der zusätzlichen Funktionalität der NamedList-Klasse nicht interessiert und benutzen ausschließlich die Methoden, die für String bzw. List-Instanzen geschrieben wurden. Durch die Parameterübergabe an Funktionen wird dieses Verfahren also sehr häufig verwendet, auch wenn man es nicht so offensichtlich sieht, wie es hier beschrieben ist.

Lösung 2: Festlegen der zu rufenden Funktionen beim Aufruf

Wie schon geschrieben ist Möglichkeit eins innerhalb einer Funktion eher umständlich, da hierfür erst eine neue Variable angelegt werden muss. Daher gibt es einen einfacheren Weg: Hierzu gibt man zusätzlich an, zu welcher Klasse die zu rufende Methode gehören soll.

int main( void )
{
  NamedList myList;
 
  printf( "Length String: %d\n", myList.String::GetLength() );
  printf( "Length Liste: %d\n", myList.List::GetLength() );
 
  return EXIT_SUCCESS;
}

Hierfür wird keine Variable benötigt, aber das Ergebnis entspricht der vorherigen Lösung:

Length String: 42
Length Liste: 10

Diese Schreibweise ist für den Compiler die Information, welchen der beiden Möglichkeiten er beim jeweiligen Aufruf nutzen möchte.

Lösung 3: Festlegen der Funktion, die für NamedList gerufen werden soll

Wenn man sich sehr sicher ist, dass nahezu ausschließlich eine Methode gemeint ist, dann kann man dies dem Datentyp NamedList auch mitteilen. Wird der Name der Liste zum Beispiel nur einmalig gesetzt und beim Bearbeiten der NamedList interessiert sich niemand für den Namen, so ist es müßig jedesmal mylist.List::GetLength() zu schrieben. Wir teilen dem Datentyp mit, dass er für GetLength die Methode aus der Liste verwenden soll. Dies geschieht in der Klassendeklaration. Dies hebelt die beiden zuvor genannten Lösungen nicht aus. So kann zum Beispiel eine zusätzliche Methode den Zugriff auf die String-Länge vereinfachen:

class NamedList : public List
                , public String
{
public: 
  using List::GetLength;
 
  inline unsigned GetStringLength() 
  { 
    return String::GetLength(); 
  } 
};
 
 
 
int main( void )
{
  NamedList myList;
 
  // Dieser Aufruf ist nur möglich, wenn in NamedList die using Anweisung vorgibt, wie GetLength() verwendet werden soll
  printf( "Length: %d\n", myList.GetLength() );
  printf( "StringLength: %d\n", myList.GetStringLength() );
 
  return EXIT_SUCCESS;
}

Da die beiden GetLength()-Methoden miteinander kollidieren, wird keine von beiden in den Namensraum der Klasse NamedList übernommen. Mit using List::GetLength wird nun der Name GetLength aus dem Namensraum der Liste übernommen und ist damit als Methode für NamedList-Instanzen verfügbar. Dabei spielt es keine Rolle, welche Rückgabetypen oder Übergabeparameter die Methode hat - dies ist wichtig beim Thema Überladung.

Auch dieser Code lässt sich natürlich kompilieren und führt zu folgender Ausgabe:

Length: 10
StringLength: 42

Das ganze ist als Beispielprojekt verfügbar: Download