Jak dotąd wszystkie przedstawione przez nas przykłady dziedziczenia były dziedziczeniem pojedynczym — to znaczy, że każda dziedziczona klasa ma jedną i tylko jedną rodzic. Jednakże C++ umożliwia wielokrotne dziedziczenie. Dziedziczenie wielokrotne umożliwia klasie pochodnej dziedziczenie elementów od więcej niż jednego rodzica.
Powiedzmy, że chcemy napisać program do śledzenia grupy nauczycieli. Nauczyciel to osoba. Nauczyciel jest jednak także pracownikiem (jeśli pracuje na własny rachunek, jest swoim własnym pracodawcą). Dziedziczenie wielokrotne można wykorzystać do utworzenia klasy Nauczyciela, która dziedziczy właściwości zarówno po osobie, jak i po pracowniku. Aby skorzystać z dziedziczenia wielokrotnego, po prostu określ każdą klasę bazową (tak jak w przypadku dziedziczenia pojedynczego), oddzieloną przecinkiem.

#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
A mixin (pisane również jako „mix-in”) to mała klasa, z której można dziedziczyć w celu dodania właściwości do klasy. Nazwa mixin wskazuje, że klasa jest przeznaczona do mieszania z innymi klasami, a nie do samodzielnego tworzenia instancji.
W poniższym przykładzie klasy Box, Label, I Tooltip to miksy, z których dziedziczymy w celu utworzenia nowego Button .
// 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 Tooltip // mixin Tooltip class
{
public:
void setText(const std::string_view str) { m_text = str; }
private:
std::string m_text{};
};
class Button : public Box, public Label, public Tooltip {}; // Button using three mixins
int main()
{
Button button{};
button.Box::setTopLeft({ 1, 1 });
button.Box::setBottomRight({ 10, 10 });
button.Label::setText("Submit");
button.Label::setFontSize(6);
button.Tooltip::setText("Submit the form to the server");
}Możesz się zastanawiać, dlaczego używamy jawnych Box::, Label::, I Tooltip:: przedrostków rozdzielczości zakresu, gdy w większości nie jest to konieczne przypadki.
Label::setText()iTooltip::setText()mają ten sam prototyp. Gdybyśmy wywołalibutton.setText(), kompilator wygenerowałby niejednoznaczny błąd kompilacji wywołania funkcji. W takich przypadkach musimy użyć przedrostka, aby ujednoznacznić, której wersji chcemy.- W niejednoznacznych przypadkach użycie nazwy miksu zapewnia dokumentację dotyczącą tego, do którego miksu odnosi się wywołanie funkcji, co pomaga uczynić nasz kod łatwiejszym do zrozumienia.
- Niejednoznaczne przypadki mogą w przyszłości stać się niejednoznaczne, jeśli dodamy dodatkowe miksy. Używanie jawnych przedrostków pomaga temu zapobiec.
Dla zaawansowanych czytelników
Ponieważ miksy zaprojektowano tak, aby dodawać funkcjonalność do klasy pochodnej, a nie zapewniać interfejs, miksiny zazwyczaj nie używają funkcji wirtualnych (omówione w następnym rozdziale). Zamiast tego, jeśli klasa miksowania musi zostać dostosowana do określonego działania, zwykle używane są szablony. Z tego powodu klasy mixin są często oparte na szablonach.
Być może, co zaskakujące, klasa pochodna może dziedziczyć z klasy bazowej mixin, używając klasy pochodnej jako parametru typu szablonu. Takie dziedziczenie nazywa się Ciekawie powtarzającym się wzorcem szablonu (w skrócie CRTP), który wygląda tak:
// 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>
{
};Prosty przykład można znaleźć za pomocą CRTP tutaj.
Problemy z dziedziczeniem wielokrotnym
Podczas gdy dziedziczenie wielokrotne wydaje się prostym rozszerzeniem pojedynczego dziedziczenia, dziedziczenie wielokrotne wprowadza wiele problemów, które mogą znacznie zwiększyć złożoność programów i sprawić, że będą koszmarem w utrzymaniu. Przyjrzyjmy się niektórym z takich sytuacji.
Po pierwsze, niejednoznaczność może wystąpić, gdy wiele klas bazowych zawiera funkcję o tej samej nazwie. Na przykład:
#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;
}Po wywołaniu c54G.getID() jest skompilowany, kompilator sprawdza, czy WirelessAdapter zawiera funkcję o nazwie getID(). Tak nie jest. Następnie kompilator sprawdza, czy którakolwiek z klas nadrzędnych ma funkcję o nazwie getID(). Widzisz problem tutaj? Problem polega na tym, że c54G faktycznie zawiera DWIE funkcje getID(): jedną odziedziczoną z USBDevice i jedną odziedziczoną z NetworkDevice. W związku z tym wywołanie tej funkcji jest niejednoznaczne i przy próbie jej skompilowania pojawi się błąd kompilatora.
Istnieje jednak sposób na obejście tego problemu: możesz jawnie określić, którą wersję chcesz wywołać:
int main()
{
WirelessAdapter c54G { 5442, 181742 };
std::cout << c54G.USBDevice::getID();
return 0;
}Chociaż to obejście jest dość proste, możesz zobaczyć, jak sprawy mogą się skomplikować, gdy twoja klasa dziedziczy z czterech lub sześciu klas podstawowych, które same dziedziczą z innych klas. Potencjał konfliktów nazewnictwa wzrasta wykładniczo w miarę dziedziczenia większej liczby klas i każdy z tych konfliktów nazewnictwa musi zostać rozwiązany jawnie.
Drugi, poważniejszy jest problem z diamentem, który Twój autor lubi nazywać „diamentem zagłady”. Dzieje się tak, gdy klasa mnoży dziedziczy z dwóch klas, z których każda dziedziczy z jednej klasy bazowej. Prowadzi to do wzorca dziedziczenia w kształcie rombu.
Rozważmy na przykład następujący zestaw klas:
class PoweredDevice
{
};
class Scanner: public PoweredDevice
{
};
class Printer: public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
Zarówno skanery, jak i drukarki są urządzeniami zasilanymi, więc wywodzą się z PoweredDevice. Jednakże kopiarka łączy w sobie funkcjonalność zarówno skanera, jak i drukarki.
W tym kontekście pojawia się wiele problemów, w tym to, czy kopiarka powinna mieć jedną czy dwie kopie PoweredDevice oraz jak rozwiązywać niektóre typy niejednoznacznych odniesień. Chociaż większość tych problemów można rozwiązać poprzez wyraźne określenie zakresu, koszty utrzymania dodane do klas w celu poradzenia sobie z dodatkową złożonością mogą spowodować, że czas programowania gwałtownie się wydłuży. O sposobach rozwiązania problemu diamentu porozmawiamy więcej w następnym rozdziale (lekcja 25.8 -- Wirtualne klasy bazowe).
Czy dziedziczenie wielokrotne to więcej kłopotów, niż jest warte?
Jak się okazuje, większość problemów, które można rozwiązać za pomocą dziedziczenia wielokrotnego, można rozwiązać również za pomocą pojedynczego dziedziczenia. Wiele języków obiektowych (np. Smalltalk, PHP) nie obsługuje nawet wielu dziedziczenie. Wiele stosunkowo nowoczesnych języków, takich jak Java i C#, ogranicza klasy do pojedynczego dziedziczenia normalnych klas, ale pozwala na wielokrotne dziedziczenie klas interfejsu (o czym porozmawiamy później). Główną ideą zakazu wielokrotnego dziedziczenia w tych językach jest to, że po prostu sprawia to, że język jest zbyt skomplikowany i ostatecznie powoduje więcej problemów niż rozwiązuje.
Wielu autorów i doświadczonych programistów uważa, że za wszelką cenę należy unikać wielokrotnego dziedziczenia w C++ ze względu na wiele potencjalnych problemów, jakie stwarza. Twój autor nie zgadza się z tym podejściem, ponieważ są chwile i sytuacje, w których dziedziczenie wielokrotne jest najlepszym sposobem postępowania. Jednakże dziedziczenia wielokrotnego należy używać niezwykle rozważnie.
Co ciekawe, korzystałeś już z klas napisanych przy użyciu dziedziczenia wielokrotnego, nie wiedząc o tym: obiekty biblioteki iostream std::cin i std::cout są implementowane przy użyciu wielokrotnego dziedziczenia!
Najlepsza praktyka
Unikaj wielokrotnego dziedziczenia. dziedziczenie, chyba że alternatywy prowadzą do większej złożoności.

