До сих пір ми розглядали тільки одиночні спадкування, коли дочірній клас має тільки одного батька. Однак C++ надає можливість множинного спадкування.
Множинне спадкування
Множинне спадкування дозволяє одному дочірньому класу мати кілька батьків. Припустимо, що ми хочемо написати програму для відстеження роботи вчителів. Учитель — це Human. Тим не менш, він також є Працівником (Employee).
Множинне спадкування може бути використано для створення класу Teacher, який успадковуватиме властивості як Human, так і Employee. Для використання множинного спадкування потрібно просто вказати через кому тип спадкування і другий батьківський клас:
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 <string> class Human { private: std::string m_name; int m_age; public: Human(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 відкрито наслідує властивості класів Human і Employee class Teacher: public Human, public Employee { private: int m_teachesGrade; public: Teacher(std::string name, int age, std::string employer, double wage, int teachesGrade) : Human(name, age), Employee(employer, wage), m_teachesGrade(teachesGrade) { } }; |
Тут ми використовуємо спадкування типу public.
Проблеми з множинним спадкуванням
Хоча множинне спадкування здається простим розширенням одиночного спадкування, воно може призвести до безлічі проблем, які можуть помітно збільшити складність програм і зробити кошмаром подальшу підтримку коду. Розглянемо деякі з подібних ситуацій.
По-перше, може виникнути неоднозначність, коли кілька батьківських класів мають метод з одним і тим же ім’ям, наприклад:
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 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(6334, 292651); std::cout << c54G.getID(); // яку версію getID() тут слід викликати? return 0; } |
При компіляції c54G.getID()
компілятор дивиться, чи є у WirelessAdapter метод getID(). Цього методу у нього немає, тому компілятор рухається по “ланцюжку” спадкування вгору і дивиться, чи є цей метод в будь-якому з батьківських класів. І тут виникає проблема — getID() є як у USBDevice, так і у NetworkDevice. Отже, виклик цього методу призведе до неоднозначності і ми отримаємо помилку, тому що компілятор не знатиме, яку версію getID() йому слід викликати.
Тим не менш, є спосіб обійти цю проблему. Ми можемо явно вказати, яку версію getID() слід викликати:
1 2 3 4 5 6 7 |
int main() { WirelessAdapter c54G(6334, 292651); std::cout << c54G.USBDevice::getID(); return 0; } |
Хоча це рішення досить просте, але все може стати набагато складніше, якщо наш клас матиме від 4 батьківських класів, які, в свою чергу, матимуть свої батьківські класи. Можливість виникнення конфліктів імен збільшується експоненціально з кожним доданим батьківським класом, і в кожному з таких випадків потрібно буде явно вказувати версії методів, які слід викликати, щоб уникнути можливості виникнення конфліктів імен.
По-друге, більш серйозною проблемою є «алмаз смерті» (або «алмаз приреченості»). Це ситуація, коли один клас має 2 батьківських класи, кожен з яких, в свою чергу, успадковує властивості одного і того ж батьківського класу. Ілюстративно ми отримуємо форму алмазу.
Наприклад, розглянемо наступні класи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class PoweredDevice { }; class Scanner: public PoweredDevice { }; class Printer: public PoweredDevice { }; class Copier: public Scanner, public Printer { }; |
Сканери та принтери — це пристрої, які отримують живлення від розетки, тому вони наслідують властивості PoweredDevice. Однак ксерокс (Copier) включає в себе функції як сканерів, так і принтерів.
У цьому контексті виникає багато проблем, включаючи неоднозначність при виклику методів і копіювання даних PoweredDevice в клас Copier двічі. Хоча більшість з цих проблем можна вирішити за допомогою явної вказівки, підтримка і обслуговування такого коду може призвести до непередбачуваних затрат часу. Ми поговоримо детально про способи вирішення проблеми “алмаз смерті” на відповідному уроці.
Чи варто використовувати множинне спадкування?
Більшість завдань, які вирішуються за допомогою множинного спадкування, можна вирішити і з використанням одиночного спадкування. Є об’єктно-орієнтовані мови програмування (наприклад, Smalltalk, PHP), які навіть не підтримують множинне спадкування. Багато відносно сучасних мов програмування, таких як Java і C#, обмежують класи одиночним спадкуванням звичайних класів, але допускають множинне спадкування інтерфейсних класів. Суть ідеї, яка забороняє множинне спадкування в цих мовах, полягає в тому, що це зайва складність, яка породжує більше проблем, ніж вигод.
Багато досвідчених програмістів вважають, що множинне спадкування в мові C++ слід уникати будь-якою ціною через потенційні проблеми, які можуть виникнути. Однак все ж залишається ймовірність, коли множинне спадкування буде кращим рішенням, ніж придумування дворівневих “костилів”.
Варто відзначити, що ви самі вже використовували класи, написані з використанням множинного спадкування, навіть не підозрюючи про це: такі об’єкти, як std::cin і std::cout бібліотеки iostream, реалізовані з використанням множинного спадкування!
Правило: Використовуйте множинне спадкування тільки в крайніх випадках, коли завдання не можна вирішити одиночним спадкуванням, або іншим альтернативним способом (без придумування “велосипеду”).