17.9 – Множественное наследование

Добавлено 1 августа 2021 в 16:02

До сих пор все представленные нами примеры наследования были одиночными, то есть каждый наследующий класс имеет одного и только одного родителя. Однако C++ предоставляет возможность множественного наследования. Множественное наследование позволяет производному классу наследовать члены более чем от одного родителя.

Допустим, мы хотим написать программу, чтобы отслеживать группу преподавателей. Преподаватель – это человек. Однако преподаватель также является сотрудником (и, если он работает на себя, является работодателем). Множественное наследование можно использовать для создания класса Teacher (преподаватель), который наследует свойства как от Person (человек), так и от Employee (сотрудник). Чтобы использовать множественное наследование, просто укажите все базовые классы (как и в одиночном наследовании), разделив их запятыми.

Рисунок 1 Диаграмма наследования
Рисунок 1 – Диаграмма наследования
#include <string>
 
class Person
{
private:
    std::string m_name;
    int m_age;
 
public:
    Person(std::string name, int age)
        : m_name(name), m_age(age)
    {
    }
 
    std::string getName() { return m_name; }
    int getAge() { return m_age; }
};
 
class Employee
{
private:
    std::string m_employer;
    double m_wage;
 
public:
    Employee(std::string employer, double wage)
        : m_employer(employer), m_wage(wage)
    {
    }
 
    std::string getEmployer() { return m_employer; }
    double getWage() { return m_wage; }
};
 
// Teacher открыто наследует Person и Employee
class Teacher: public Person, public Employee
{
private:
     int m_teachesGrade;
 
public:
    Teacher(std::string name, int age, std::string employer, double wage, int teachesGrade)
        : Person(name, age), Employee(employer, wage), m_teachesGrade(teachesGrade)
    {
    }
};

Проблемы с множественным наследованием

Хотя множественное наследование кажется простым расширением одиночного наследования, оно создает множество проблем, которые могут заметно увеличить сложность программ и сделать их кошмаром для поддержки. Давайте посмотрим на некоторые из этих ситуаций.

Во-первых, неоднозначность может возникнуть, если несколько базовых классов содержат функцию с одним и тем же именем. Например:

#include <iostream>
 
class USBDevice
{
private:
    long m_id;
 
public:
    USBDevice(long id)
        : m_id(id)
    {
    }
 
    long getID() { return m_id; }
};
 
class NetworkDevice
{
private:
    long m_id;
 
public:
    NetworkDevice(long id)
        : m_id(id)
    {
    }
 
    long getID() { 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(); // какую getID() мы вызываем?
 
    return 0;
}

Когда c54G.getID() компилируется, компилятор проверяет, содержит ли WirelessAdapter функцию с именем getID(). Ее у него нет. Затем компилятор проверяет, есть ли в каком-либо из родительских классов функция с именем getID(). Видите здесь проблему? Проблема в том, что c54G на самом деле содержит ДВЕ функции getID(): одна унаследована от USBDevice, а другая – от NetworkDevice. Следовательно, этот вызов функции неоднозначен, и вы получите ошибку компиляции, если попытаетесь скомпилировать этот код.

Однако есть способ обойти эту проблему: вы можете явно указать, какую версию вы хотели вызвать:

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

Хотя этот обходной путь довольно прост, вы можете увидеть, как всё может усложниться, когда ваш класс наследуется от четырех или шести базовых классов, которые сами наследуются от других классов. По мере того, как вы наследуете больше классов, вероятность конфликтов имен возрастает экспоненциально, и каждый из этих конфликтов имен должен быть разрешен явным образом.

Во-вторых, более серьезная проблема – проблема ромба (или англоязычный термин – diamond problem). Она появляется, когда класс множественно наследуется от двух классов, каждый из которых наследуется от одного базового класса. Это приводит к ромбовидной структуре наследования.

Например, рассмотрим следующий набор классов:

class PoweredDevice
{
};
 
class Scanner: public PoweredDevice
{
};
 
class Printer: public PoweredDevice
{
};
 
class Copier: public Scanner, public Printer
{
};
Рисунок 2 Ромбовидная структура наследования
Рисунок 2 – Ромбовидная структура наследования

Сканеры (Scanner) и принтеры (Printer) являются устройствами с питанием, поэтому они являются производными от PoweredDevice. Однако копировальный аппарат (Copier) включает в себя функции как сканеров, так и принтеров.

В этом контексте возникает много вопросов, в том числе, должен ли Copier иметь одну или две копии PoweredDevice, и как разрешать определенные типы неоднозначных ссылок. Хотя большинство этих проблем можно решить с помощью явного определения области видимости, затраты на поддержку, добавленные к вашим классам, чтобы справиться с дополнительной сложностью, могут привести к резкому увеличению времени разработки. Подробнее о способах решения проблемы ромба мы поговорим в следующей главе (урок «18.8 – Виртуальные базовые классы»).

Множественное наследование – больше проблем, чем оно того стоит?

Как оказалось, большинство задач, которые можно решить с помощью множественного наследования, можно решить и с помощью одиночного наследования. Многие объектно-ориентированные языки (например, Smalltalk, PHP) даже не поддерживают множественное наследование. Многие относительно современные языки, такие как Java и C#, ограничивают классы одним наследованием от обычных классов, но допускают множественное наследование от классов интерфейсов (о чем мы поговорим позже). Основная идея запрета множественного наследования в этих языках заключается в том, что это просто делает язык слишком сложным и в конечном итоге вызывает больше проблем, чем устраняет.

Многие авторы и опытные программисты считают, что множественного наследования в C++ следует избегать любой ценой из-за множества потенциальных проблем, которые оно приносит. Автор данного руководства не согласен с этим подходом, потому что бывают случаи и ситуации, когда множественное наследование – лучший способ решения задачи. Однако множественное наследование следует использовать крайне осторожно.

Интересно отметить, что вы уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: объекты библиотеки iostream std::cin и std::cout реализованы с использованием множественного наследования!

Правило


Избегайте множественного наследования, если альтернативы не приведут к усложнению.

Теги

C++ / CppLearnCppДля начинающихКласс (программирование)Множественное наследованиеНаследованиеОбучениеПроблема ромба / Diamond problemПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.