На этом уроке мы рассмотрим, как выводить объекты классов через оператор вывода в языке С++.
Проблема с переопределением 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; } |
Вышеприведенная программа работает во всех трех случаях:
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(), как мы и предполагали.
Проблема решена.
Так удобней:
Наверное, подразумевается следующий код.
Хотелось бы еще узнать мнения, какой способ предпочтительней и почему (кроме того, что дружественные операторы — гадость).
Я так понимаю, что Вы в ответ на мой пост писали.
Я же на Страуструпа ссылался 🙂 Зачем переизобретать велосипед? Почитайте, чел наверное немного разбирается в плюсах 🙂
в кратце: мы не для того инкапсулировались, чтобы тут же "пускать друзей" 🙂
ну а как по мне, то функцию print можно сделать управляемой (добавить параметры "как именно выводить" с дефолтными значениями) — это удобно. Но при этом для самого очевидного действия можно перегрузить оператор вывода в вызовом дефолтной print.
Что мешает сделать приведение? Ведь намного легче? Или будет код грязный—и непонятный—?
Вот если бы Вы пояснили, как Вы это здесь представляете, Вам бы ответили. Я подумала пару минут над Вашим вопросом, но так и не поняла, причём здесь это. Привести пэрента к чайлд или наоборот? Дак они и выводить должны разные вещи. Если ссылку пэрент на чайлд привести к чайлд, то будет работать перегрузка оператора для чайлд, которой нет. А если сработает родительская перегрузка оператора, то и выводить он будет родителя, если не придумать доп виртуальную функцию, как здесь показано. Или я чего-то не понимаю, или Вы…
Это не всегда удобно. Если представить, что у тебя есть массив указателей на Parent, и туда запихиваются множество дочерних классов, то ты хочешь в одном цикле их всех вывести:
Как в данном случае делать приведение? Конечно, можно сделать костыли и написать что-то уродливое, но вариант, который описан в лекции приводит к лаконичному и аккуратному коду в 1 СТРОЧКУ!
Собственно, в каком-то давнем уроке я писал о том, что дружественные операторы/функции — это гадость и их следует избегать (ссылаясь на Страуструпа)… В этом уроке очень классно показано, почему это так… но при этом мы продолжаем использовать дружественную перегрузку… зачем? Что теперь то мешает выбросить перегрузку оператора из класса, если он и так использует виртуальный метод?
Так без него не будет работать вывод объекта, который хотел получить автор в начале статьи
Да что Вы говорите?
А у меня работает… как же так?
Что у вас работает? Код в студию