Що ви опануєте
Ви навчитеся визначати спільний поведінковий контракт, створювати похідні класи й викликати їх через базовий інтерфейс. Зможете пояснити virtual, override, абстрактний клас, зрізання об’єкта та потребу віртуального деструктора. Приклад — різні способи оцінювання навчальної відповіді.
Наслідування як відношення контрактів
Наслідування дозволяє похідному класу розширювати або спеціалізувати базовий тип. Публічне наслідування має підтримувати використання похідного об’єкта там, де очікується базовий контракт. Якщо новий тип посилює передумови або порушує гарантії базового, спільний інтерфейс стає ненадійним для користувача.
Поліморфізм означає використання різних реалізацій через узгоджений інтерфейс. Для динамічного поліморфізму virtual функція обирається за фактичним типом живого об’єкта під час виконання. Абстрактний клас має принаймні одну чисто віртуальну функцію = 0 і не може бути створений безпосередньо. Він може задавати контракт без даних.
Повний приклад
#include <iostream>
#include <memory>
#include <string_view>
#include <vector>
class Grader {
public:
virtual ~Grader() = default; // Безпечне видалення через базовий тип.
virtual int grade(std::string_view answer) const = 0;
};
class ExactGrader final : public Grader {
public:
int grade(std::string_view answer) const override {
return answer == "RAII" ? 100 : 0;
}
};
class NonEmptyGrader final : public Grader {
public:
int grade(std::string_view answer) const override {
return answer.empty() ? 0 : 50;
}
};
int main() {
std::vector<std::unique_ptr<Grader>> graders;
graders.push_back(std::make_unique<ExactGrader>());
graders.push_back(std::make_unique<NonEmptyGrader>());
for (const auto& grader : graders) {
std::cout << grader->grade("RAII") << '\n';
}
}
Очікуваний вивід — 100 і 50. Базовий контракт приймає будь-яке string_view на живі дані та повертає оцінку 0–100. override просить компілятор перевірити відповідність сигнатури базовій virtual функції. final забороняє подальше наслідування цього класу. -> викликає операцію через вказівник, що міститься в unique_ptr.
Динамічний виклик
Колекція зберігає власників базового типу, але об’єкти мають конкретні похідні типи. Виклик grade знаходить реалізацію за динамічним типом. Це дозволяє обробляти всі оцінювачі одним циклом. Реалізація virtual dispatch часто використовує таблиці віртуальних функцій, проте стандарт задає поведінку, а не конкретну структуру таблиці.
Поліморфізм потребує обґрунтованої варіативності. Якщо відома проста множина незалежних функцій, можна використати композицію або передавати callable. Вибір має враховувати спосіб розширення й читання коду. Ієрархія з багатьма рівнями може створити залежності, які важче простежити, ніж самі відмінності поведінки.
Віртуальний деструктор
Коли об’єкт видаляється через вказівник на базовий клас, деструктор базового має підтримувати правильне знищення похідного. У прикладі unique_ptr<Grader> при завершенні життя викликає видалення через Grader*. Віртуальний деструктор забезпечує виклик потрібної похідної частини. Його = default залишає стандартну реалізацію, оскільки ручних ресурсів у базовому класі немає.
Базовий клас для поліморфного видалення зазвичай має публічний virtual деструктор. Інші дизайни можуть забороняти видалення через базу захищеним невіртуальним деструктором, але це потребує іншого контракту використання. Для навчального контейнера обрано перший варіант.
Зрізання й позичені дані
Object slicing виникає, коли похідний об’єкт копіюють у базовий об’єкт за значенням: похідна частина не входить у результат. Для абстрактної бази таке створення неможливе, що також допомагає уникнути помилки. Для не абстрактної бази поліморфні колекції зазвичай зберігають вказівники чи власники, а не базові значення.
std::string_view позичає символи та не продовжує їхнього життя. У виклику з літералом дані мають достатню тривалість. Зберігати view у похідному класі після тимчасового рядка без гарантії життя небезпечно. Наші методи використовують відповідь лише під час виклику й не зберігають доступ, що потрібно зазначити в контракті.
Перевірка поведінкового контракту
Для кожного оцінювача перевірте порожню відповідь, правильну й іншу непорожню. Усім реалізаціям потрібна гарантія результату 0–100. Якщо новий оцінювач повертає 200 або вимагає непорожню відповідь без погодження базового контракту, користувач циклу вже не може покладатися на інтерфейс. Тести базового контракту можна застосовувати до кожної реалізації.
Не використовуйте перевірку типу й приведення до кожного похідного лише для виклику спільної дії. Якщо дії справді різні, перегляньте межі інтерфейсу. Поліморфний контракт має пояснювати те, що спільний користувач може очікувати без знання конкретного класу.
Типові помилки та професійна практика
Типові помилки: відсутній virtual деструктор, помилкова сигнатура без override, збереження позичених тимчасових даних та ієрархія лише для повторного використання поля. protected дані, доступні багатьом похідним, ускладнюють контроль інваріантів. Віддавайте перевагу вузькому поведінковому контракту.
Практичний аналіз
Спроєктуйте оцінювач, який перевіряє довжину відповіді за погодженим правилом. Поясніть різницю між байтами й символами UTF-8 перед використанням size(). Визначте тести контракту та життєвий цикл об’єкта в колекції. Чому копія базового значення не є заміною поліморфному власнику?
Контракт базового типу та підстановка
Поліморфний код знає контракт Grader і викликає grade без знання конкретного класу. Тому всі реалізації повинні узгоджувати зміст результату: наприклад, true означає відповідність правилам цього оцінювача. Якщо один клас почне використовувати true як сигнал помилки, спільний цикл втратить коректну інтерпретацію.
Принцип підстановки вимагає, щоб клієнт базового контракту міг працювати з похідним об’єктом без порушення очікувань. Похідний метод не повинен несподівано вимагати сильніших передумов або послаблювати обіцяний результат. Якщо база приймає порожню відповідь і повертає false, похідний клас не має мовчки трактувати її як завершення програми.
Override просить компілятор перевірити відповідність сигнатури віртуальній функції. Помилка у const або типі параметра тоді виявиться при компіляції. Віртуальний деструктор забезпечує коректне знищення через базовий вказівник; це важливо для unique_ptr<Grader>, який володіє конкретною похідною реалізацією.
Обираючи між наслідуванням і композицією, визначте потрібну взаємозамінність. Композиція означає, що об’єкт містить інший об’єкт або співпрацює з ним. Наприклад, сервіс оцінювання може містити Grader та окремий журнал результатів. Зберігання журналу всередині кожного похідного класу дублюватиме відповідальність без потреби. Для власного прикладу сформулюйте один спільний контракт та два способи його реалізації, перевіряючи однакові граничні входи.
Схема процесу
Задаємо спільний контракт.
Самоперевірка
Як обґрунтувати відповідь своїми словами?
Підсумок
Поліморфізм дозволяє змінювати реалізації за стабільного поведінкового контракту. Override, virtual деструктор та явне володіння роблять використання безпечнішим. Передумови й результати мають підтримувати підстановку всіх похідних типів.