17.9 – Mehrfachvererbung

17.9 – Mehrfachvererbung

Bisher waren alle Beispiele für Vererbung, die wir vorgestellt haben, Einzelvererbung – das heißt, jede vererbte Klasse hat genau einen Elternteil. C++ bietet jedoch die Möglichkeit, Mehrfachvererbung durchzuführen. Mehrfachvererbung ermöglicht es einer abgeleiteten Klasse, Mitglieder von mehr als einem Elternteil zu erben.

Nehmen wir an, wir wollten ein Programm schreiben, um eine Gruppe von Lehrern im Auge zu behalten. Ein Lehrer ist eine Person. Ein Lehrer ist jedoch auch ein Angestellter (er ist sein eigener Arbeitgeber, wenn er für sich selbst arbeitet). Mehrfachvererbung kann verwendet werden, um eine Teacher-Klasse zu erstellen, die Eigenschaften sowohl von Person als auch von Employee erbt. Um die Mehrfachvererbung zu verwenden, geben Sie einfach jede Basisklasse (genau wie bei der Einfachvererbung) durch ein Komma getrennt an.

#include <string>
#include <string_view>

class Person
{
private:
    std::string m_name;
    int m_age{};

public:
    Person(std::string_view name, int age)
        : m_name{ name }, m_age{ age }
    {
    }

    const std::string& getName() const { return m_name; }
    int getAge() const { return m_age; }
};

class Employee
{
private:
    std::string m_employer;
    double m_wage{};

public:
    Employee(std::string_view employer, double wage)
        : m_employer{ employer }, m_wage{ wage }
    {
    }

    const std::string& getEmployer() const { return m_employer; }
    double getWage() const { return m_wage; }
};

// Teacher publicly inherits Person and Employee
class Teacher : public Person, public Employee
{
private:
    int m_teachesGrade{};

public:
    Teacher(std::string_view name, int age, std::string_view employer, double wage, int teachesGrade)
        : Person{ name, age }, Employee{ employer, wage }, m_teachesGrade{ teachesGrade }
    {
    }
};

int main()
{
    Teacher t{ "Mary", 45, "Boo", 14.3, 8 };

    return 0;
}

Mixins

Ein Mixin (auch „Mix-In“ geschrieben) ist eine kleine Klasse, von der geerbt werden kann, um einer Klasse Eigenschaften hinzuzufügen. Der Name Mixin gibt an, dass die Klasse dazu bestimmt ist, in andere Klassen gemischt und nicht eigenständig instanziiert zu werden.

Im folgenden Beispiel der Box und Label Klassen sind Mixins, von denen wir erben, um einen neuen Button zu erstellen Klasse.

// h/t to reader Waldo for this example
#include <string>

struct Point2D
{
	int x;
	int y;
};

class Box // mixin Box class
{
public:
	void setTopLeft(Point2D point) { m_topLeft = point; }
	void setBottomRight(Point2D point) { m_bottomRight = point; }
private:
	Point2D m_topLeft{};
	Point2D m_bottomRight{};
};

class Label // mixin Label class
{
public:
	void setText(const std::string_view str) { m_text = str; }
	void setFontSize(int fontSize) { m_fontSize = fontSize; }
private:
	std::string m_text{};
	int m_fontSize{};
};

class Button : public Box, public Label {};

int main()
{
	Button button{};
	button.Box::setTopLeft({ 1, 1 });
	button.Box::setBottomRight({ 10, 10 });
	button.Label::setText("Username: ");
	button.Label::setFontSize(6);
}

Für fortgeschrittene Leser

Da Mixins entwickelt wurden, um der abgeleiteten Klasse Funktionalität hinzuzufügen und keine Schnittstelle bereitzustellen, verwenden Mixins normalerweise keine virtuellen Funktionen (die im nächsten Kapitel behandelt werden). Wenn eine Mixin-Klasse angepasst werden muss, um auf eine bestimmte Weise zu funktionieren, werden stattdessen normalerweise Vorlagen verwendet. Aus diesem Grund werden Mixin-Klassen oft mit Vorlagen erstellt.

Vielleicht überraschenderweise kann eine abgeleitete Klasse von einer Mixin-Basisklasse erben, indem sie die abgeleitete Klasse als Vorlagentypparameter verwendet. Eine solche Vererbung wird Curiously Recurring Template Pattern (kurz CRTP) genannt und sieht folgendermaßen aus:

// The Curiously Recurring Template Pattern (CRTP)

template <class T>
class Mixin
{
    // Mixin<T> can use template type parameter T to access members of Derived
    // via (static_cast<T*>(this))
};

class Derived : public Mixin<Derived>
{
};

Ein einfaches Beispiel mit CRTP finden Sie hier.

Probleme mit Mehrfachvererbung

Während die Mehrfachvererbung wie eine einfache Erweiterung der Einfachvererbung erscheint, bringt die Mehrfachvererbung viele Probleme mit sich, die die Komplexität von Programmen deutlich erhöhen und sie zu einem Alptraum bei der Wartung machen können. Sehen wir uns einige dieser Situationen an.

Erstens kann es zu Mehrdeutigkeiten kommen, wenn mehrere Basisklassen eine Funktion mit demselben Namen enthalten. Zum Beispiel:

#include <iostream>

class USBDevice
{
private:
    long m_id {};

public:
    USBDevice(long id)
        : m_id { id }
    {
    }

    long getID() const { return m_id; }
};

class NetworkDevice
{
private:
    long m_id {};

public:
    NetworkDevice(long id)
        : m_id { id }
    {
    }

    long getID() const { return m_id; }
};

class WirelessAdapter: public USBDevice, public NetworkDevice
{
public:
    WirelessAdapter(long usbId, long networkId)
        : USBDevice { usbId }, NetworkDevice { networkId }
    {
    }
};

int main()
{
    WirelessAdapter c54G { 5442, 181742 };
    std::cout << c54G.getID(); // Which getID() do we call?

    return 0;
}

Wenn c54G.getID() kompiliert ist, prüft der Compiler, ob WirelessAdapter eine Funktion namens getID() enthält. Es tut nicht. Der Compiler prüft dann, ob eine der übergeordneten Klassen eine Funktion namens getID() hat. Sehen Sie das Problem hier? Das Problem ist, dass c54G tatsächlich ZWEI getID()-Funktionen enthält:eine von USBDevice geerbte und eine von NetworkDevice geerbte. Folglich ist dieser Funktionsaufruf mehrdeutig und Sie erhalten einen Compiler-Fehler, wenn Sie versuchen, ihn zu kompilieren.

Es gibt jedoch eine Möglichkeit, dieses Problem zu umgehen:Sie können explizit angeben, welche Version aufgerufen werden soll:

int main()
{
    WirelessAdapter c54G { 5442, 181742 };
    std::cout << c54G.USBDevice::getID();

    return 0;
}

Obwohl diese Problemumgehung ziemlich einfach ist, können Sie sehen, wie komplex die Dinge werden können, wenn Ihre Klasse von vier oder sechs Basisklassen erbt, die selbst von anderen Klassen erben. Das Potenzial für Namenskonflikte steigt exponentiell, wenn Sie mehr Klassen erben, und jeder dieser Namenskonflikte muss explizit gelöst werden.

Zweitens und schwerwiegender ist das Diamantenproblem, das Ihr Autor gerne den „Diamanten des Untergangs“ nennt. Dies tritt auf, wenn eine Klasse mehrfach von zwei Klassen erbt, die jeweils von einer einzigen Basisklasse erben. Dies führt zu einem rautenförmigen Vererbungsmuster.

Betrachten Sie beispielsweise die folgende Gruppe von Klassen:

class PoweredDevice
{
};

class Scanner: public PoweredDevice
{
};

class Printer: public PoweredDevice
{
};

class Copier: public Scanner, public Printer
{
};

Sowohl Scanner als auch Drucker sind angetriebene Geräte, daher sind sie von PoweredDevice abgeleitet. Ein Kopiergerät enthält jedoch die Funktionalität von Scannern und Druckern.

In diesem Zusammenhang treten viele Probleme auf, einschließlich der Frage, ob Copier eine oder zwei Kopien von PoweredDevice haben sollte und wie bestimmte Arten von mehrdeutigen Verweisen gelöst werden können. Während die meisten dieser Probleme durch explizites Scoping angegangen werden können, kann der Wartungsaufwand, der Ihren Klassen hinzugefügt wird, um mit der zusätzlichen Komplexität fertig zu werden, die Entwicklungszeit in die Höhe schnellen lassen. Wir werden im nächsten Kapitel (Lektion 18.8 – Virtuelle Basisklassen) mehr darüber sprechen, wie wir das Diamantenproblem lösen können.

Ist Mehrfachvererbung mehr Ärger als es wert ist?

Wie sich herausstellt, können die meisten Probleme, die mit Mehrfachvererbung gelöst werden können, auch mit Einfachvererbung gelöst werden. Viele objektorientierte Sprachen (zB Smalltalk, PHP) unterstützen nicht einmal Mehrfachvererbung. Viele relativ moderne Sprachen wie Java und C# beschränken Klassen auf die einfache Vererbung normaler Klassen, erlauben aber die mehrfache Vererbung von Schnittstellenklassen (worüber wir später sprechen werden). Die treibende Idee hinter dem Verbot der Mehrfachvererbung in diesen Sprachen ist, dass die Sprache dadurch einfach zu komplex wird und letztendlich mehr Probleme verursacht als behoben werden.

Viele Autoren und erfahrene Programmierer glauben, dass Mehrfachvererbung in C++ aufgrund der vielen potenziellen Probleme, die sie mit sich bringt, um jeden Preis vermieden werden sollte. Ihr Autor ist mit diesem Ansatz nicht einverstanden, da es Zeiten und Situationen gibt, in denen Mehrfachvererbung die beste Vorgehensweise ist. Mehrfachvererbung sollte jedoch äußerst umsichtig eingesetzt werden.

Interessanterweise haben Sie bereits Klassen verwendet, die mit Mehrfachvererbung geschrieben wurden, ohne es zu wissen:Die iostream-Bibliotheksobjekte std::cin und std::cout sind beide mit Mehrfachvererbung implementiert!

Best Practice

Vermeiden Sie Mehrfachvererbung, es sei denn, Alternativen führen zu mehr Komplexität.