На цьому уроці ми розглянемо, як виводити об’єкти класів через оператор виводу в мові С++.
Проблема з перевизначенням operator<<
Розглянемо наступну програму:
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 |
#include <iostream> class Parent { public: Parent() {} virtual void print() const { std::cout << "Parent"; } }; class Child: public Parent { public: Child() {} virtual void print() const override { std::cout << "Child"; } }; int main() { Child ch; Parent &p = ch; p.print(); // викликається Child::print() return 0; } |
Тут зрозуміло, що p.print()
викликає Child::print() (оскільки p
посилається на об’єкт класу Child, то Parent::print() є віртуальною функцією, а Child::print() є перевизначенням).
Такий спосіб виводу непоганий, але з std::cout не дуже добре поєднується:
1 2 3 4 5 6 7 8 9 10 11 |
int main() { Child ch; Parent &p = ch; std::cout << "p is a "; p.print(); // розриваємо стейтмент cout заради функції print(). Непорядок! std::cout << '\n'; return 0; } |
На цьому уроці ми розглянемо, як перевизначити оператор виводу <<
для класів з спадкуванням, щоб мати можливість використовувати оператор <<
наступним чином:
1 |
std::cout << "p is a " << p << '\n'; // набагато краще |
Почнемо зі звичайного перевантаження оператора виводу <<:
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 |
#include <iostream> class Parent { public: Parent() {} virtual void print() const { std::cout << "Parent"; } friend std::ostream& operator<<(std::ostream &out, const Parent &p) { out << "Parent"; return out; } }; class Child: public Parent { public: Child() {} virtual void print() const override { std::cout << "Child"; } friend std::ostream& operator<<(std::ostream &out, const Child &ch) { out << "Child"; return out; } }; int main() { Parent p; std::cout << p << '\n'; Child ch; std::cout << ch << '\n'; return 0; } |
Оскільки тут немає віртуальних функцій, то все досить-таки просто і ясно:
Parent
Child
Тепер замінимо функцію main() на наступну:
1 2 3 4 5 6 7 8 |
int main() { Child ch; Parent &pref = ch; std::cout << pref << '\n'; return 0; } |
Результат:
Parent
А це вже не те, що нам потрібно. Оскільки перевантаження оператора <<
для об’єктів класу Parent не є віртуальним, то std::cout << pref
викликає версію оператора <<
, яка працює тільки з об’єктами класу Parent. В цьому і суть проблеми.
Чи можемо ми зробити operator<< віртуальним?
Ні, і на це є ряд причин.
По-перше, тільки методи можуть бути віртуальними. Це логічно, тому що тільки класи можуть наслідувати інші класи, і перевизначити функцію, яка знаходиться поза тілом класу — неможливо (ми можемо перевантажити функції, які не є методами, але не можемо перевизначити їх) . Оскільки оператор <<
зазвичай перевантажується через дружню функцію, а дружні функції не є методами, то дружня функція operator<< не може бути перевизначена.
По-друге, навіть якби ми могли зробити operator<< віртуальною функцією, то проблема полягає в тому, що параметри Parent::operator<< і Child::operator<< відрізняються (версія Parent приймає в якості параметру об’єкт класу Parent, а версія Child — об’єкт класу Child). Отже, версія Child не може вважатися перевизначенням версії Parent і викликатися в якості перевизначення також не може.
Рішення
Відповідь досить-таки проста.
Спочатку ми робимо operator<< дружньою функцією класу Parent. Але замість того, щоб operator<< виконував вивід самостійно, ми делегуємо цю задачу звичайному методу, який є віртуальною функцією!
Розглянемо це на практиці:
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 47 48 |
#include <iostream> class Parent { public: Parent() {} // Перевантаження оператора виводу << friend std::ostream& operator<<(std::ostream &out, const Parent &p) { // Делегуємо виконання операції виводу методу print() return p.print(out); } // Робимо метод print() віртуальним virtual std::ostream& print(std::ostream& out) const { out << "Parent"; return out; } }; class Child: public Parent { public: Child() {} // Перевизначення методу print() для роботи з об'єктами класу Child virtual std::ostream& print(std::ostream& out) const override { out << "Child"; return out; } }; int main() { Parent p; std::cout << p << '\n'; Child ch; std::cout << ch << '\n'; // зверніть увагу, все працює навіть без наявності перевантаження оператора виводу в класі Child Parent &pref = ch; std::cout << pref << '\n'; return 0; } |
Вищенаведена програма працює у всіх 3 випадках:
Parent
Child
Child
Розглянемо детально.
У випадку з об’єктом класу Parent, ми викликаємо operator<<, який викликає віртуальну функцію print(). Оскільки ми посилаємося на об’єкт класу Parent, то p.print()
викликає Parent::print(), який і виконує вивід на екран. Тут все просто.
У випадку з об’єктом класу Child, компілятор спочатку дивиться, чи є operator<<, який приймає об’єкт класу Child. Він нічого не знаходить (тому що ми це не визначили), потім дивиться, чи є operator<<, який приймає об’єкт класу Parent. Є, компілятор знаходить і виконує неявну конвертацію (підвищуюче приведення) об’єкта класу Child (посилання на об’єкт класу Child) в посилання класу Parent і викликає віртуальну функцію print(), яка, в свою чергу, викликає перевизначення Child::print().
Зверніть увагу, нам не потрібно записувати перевантаження operator<< в кожному дочірньому класі! Перевантаження, яке знаходиться в класі Parent, відмінно працює як з об’єктами класу Parent, так і з об’єктами будь-якого дочірнього класу (який наслідує клас Parent)!
В останньому випадку компілятор зіставляє посилання pref
з operator<< класу Parent. Викликається віртуальна функція print(). Оскільки посилання pref
фактично вказує на об’єкт класу Child, то викликається перевизначення Child::print(), як ми і припускали.
Проблема вирішена.