Для решения определенных проблем в наследовании в C++11 добавили два специальных модификатора: override и final. Обратите внимание, эти модификаторы не являются ключевыми словами — это обычные модификаторы, которые имеют особое значение в определенных местах использования.
Хотя final используется не часто, override же является фантастическим дополнением, которое вы должны использовать регулярно. На этом уроке мы рассмотрим оба этих модификатора, а также одно исключение из правил, когда тип возврата переопределения может не совпадать с типом возврата виртуальной функции родительского класса.
Модификатор override
Как мы уже знаем из предыдущего урока, виртуальная функция дочернего класса является переопределением, только если совпадают её сигнатура и тип возврата с сигнатурой и типом возврата виртуальной функции родительского класса. А это, в свою очередь, может привести к проблемам, когда функция, которая должна быть переопределением, на самом деле, им не является.
Рассмотрим следующий пример:
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 |
#include <iostream> class A { public: virtual const char* getName1(int x) { return "A"; } virtual const char* getName2(int x) { return "A"; } }; class B : public A { public: virtual const char* getName1(short int x) { return "B"; } // тип параметра short int virtual const char* getName2(int x) const { return "B"; } // метод является const }; int main() { B b; A &rParent = b; std::cout << rParent.getName1(1) << '\n'; std::cout << rParent.getName2(2) << '\n'; return 0; } |
Поскольку rParent
— это ссылка класса A на объект b
, то с помощью виртуальных функций мы намереваемся получить доступ к B::getName1() и к B::getName2(). Однако, поскольку в B::getName1() другой тип параметра (short int вместо int), то он не является переопределением метода A::getName1(). Более того, поскольку B::getName2() является const, а A::getName2() — нет, то B::getName2() также не считается переопределением A::getName2().
Следовательно, результат выполнения программы:
A
A
Конкретно в этом случае, поскольку A и B просто выводят свои имена, довольно легко увидеть, что что-то пошло не так, и переопределения не вызываются. Однако в более сложной программе, когда методы могут и не возвращать значения, которые выводятся на экран, найти ошибку уже будет довольно проблематично.
Для решения такого типа проблем и добавили модификатор override в C++11. Модификатор override может использоваться с любым методом, который должен быть переопределением. Достаточно просто указать override в том месте, где обычно указывается const (после скобок с параметрами). Если метод не переопределяет виртуальную функцию родительского класса, то компилятор выдаст ошибку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> class A { public: virtual const char* getName1(int x) { return "A"; } virtual const char* getName2(int x) { return "A"; } virtual const char* getName3(int x) { return "A"; } }; class B : public A { public: virtual const char* getName1(short int x) override { return "B"; } // ошибка компиляции, метод не является переопределением virtual const char* getName2(int x) const override { return "B"; } // ошибка компиляции, метод не является переопределением virtual const char* getName3(int x) override { return "B"; } // всё хорошо, метод является переопределением A::getName3(int) }; int main() { return 0; } |
Здесь мы получим две ошибки: первая для B::getName1() и вторая для B::getName2(), так как ни один из этих методов не является переопределением виртуальных функций класса А. Метод B::getName3() является переопределением, поэтому с ним никаких проблем нет.
Использование модификатора override никак не влияет на эффективность или производительность программы, но помогает избежать непреднамеренных ошибок. Следовательно, настоятельно рекомендуется использовать модификатор override для каждого из своих переопределений.
Правило: Используйте модификатор override для каждого из своих переопределений.
Модификатор final
Могут быть случаи, когда вы не хотите, чтобы кто-то мог переопределить виртуальную функцию или наследовать определенный класс. Модификатор final используется именно для этого. Если пользователь пытается переопределить метод или наследовать класс с модификатором final, то компилятор выдаст ошибку.
Указывается final в том же месте, в котором и модификатор override, например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class A { public: virtual const char* getName() { return "A"; } }; class B : public A { public: // Заметили final в конце? Это означает, что метод переопределить уже нельзя virtual const char* getName() override final { return "B"; } // всё хорошо, переопределение A::getName() }; class C : public B { public: virtual const char* getName() override { return "C"; } // ошибка компиляции: переопределение метода B::getName(), который является final }; |
В этом коде метод B::getName() переопределяет метод A::getName(). Но B::getName() имеет модификатор final, это означает, что любые дальнейшие переопределения этого метода будут вызывать ошибку компиляции. И действительно, C::getName() уже не может переопределить B::getName() — компилятор выдаст ошибку.
В случае, если мы хотим запретить наследование определенного класса, то модификатор final указывается после имени класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class A { public: virtual const char* getName() { return "A"; } }; class B final : public A // обратите внимание на модификатор final здесь { public: virtual const char* getName() override { return "B"; } }; class C : public B // ошибка компиляции: нельзя наследовать final-класс { public: virtual const char* getName() override { return "C"; } }; |
В этом примере класс B объявлен как final. Таким образом, класс C не может наследовать класс B — компилятор выдаст ошибку.
Ковариантный тип возврата
Есть один случай, когда тип возврата переопределения может не совпадать с типом возврата виртуальной функции родительского класса, но при этом оставаться переопределением. Если типом возврата виртуальной функции является указатель или ссылка на класс, то переопределения могут возвращать указатель или ссылку на свой собственный класс (т.е. вместо родительского класса указывать на дочерний класс). Это называется ковариантным типом возврата. Например:
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: // Этот метод getThis() возвращает указатель на класс Parent virtual Parent* getThis() { std::cout << "called Parent::getThis()\n"; return this; } void printType() { std::cout << "returned a Parent\n"; } }; class Child : public Parent { public: // Обычно, типы возврата переопределений и виртуальных функций родительского класса должны совпадать. // Однако, поскольку Child наследует класс Parent, следующий метод может возвращать Child* вместо Parent* virtual Child* getThis() { std::cout << "called Child::getThis()\n"; return this; } void printType() { std::cout << "returned a Child\n"; } }; int main() { Child ch; Parent *p = &ch; ch.getThis()->printType(); // вызывается Child::getThis(), возвращается Child*, вызывается Child::printType() p->getThis()->printType(); // вызывается Child::getThis(), возвращается Parent*, вызывается Parent::printType() } |
Результат выполнения программы:
called Child::getThis()
returned a Child
called Child::getThis()
returned a Parent
Некоторые старые компиляторы могут не поддерживать ковариантные типы возврата.
В примере, приведенном выше, мы сначала вызываем ch.getThis()
. Поскольку ch
является объектом класса Child, то вызывается Child::getThis(), который возвращает Child*. Этот Child* затем используется для вызова невиртуальной функции Child::printType().
Затем выполняется p->getThis()
. Переменная p
является указателем класса Parent на объект ch
класса Child. Parent::getThis() — это виртуальная функция, поэтому вызывается переопределение Child::getThis(). Хотя Child::getThis() и возвращает Child*, но, поскольку родительская часть объекта возвращает Parent*, возвращаемый Child* преобразовывается в Parent*. И, таким образом, вызывается Parent::printType().
Другими словами, в вышеприведенном примере мы получим Child* только в том случае, если будем вызывать getThis() с объектом класса Child.
А если я объявлю override дочернюю функцию, то еще более дочерние виртуальные функции автоматически станут override?
Не понимаю эту строчку в последнем примере.
Я понимаю почему вызывается Child::getThis(), который имеет тип возврата Сhild*, тогда почему он возвращает Parent*???
Здравствуйте, наверное уже поздно отвечать, но возможно мой ответ будет полезен другим.
Попробую растолковать эту строчку подробнее:
p->getThis(), поскольку этот метод виртуальный, то будет вызван «наиболее» дочерний метод, в данном случае это метод в классе Child.
Таким образом p->getThis() вернёт указатель Child, как и должен.
Но поскольку указатель p, указывает на Parent, то Child будет приведён к Parent, и далее printType(), будет вызываться уже у Parent.
P.S. Я также только учу С++, так что возможно накосячил с терминами. 🙂
P.P.S. Подробнее разберитесь с таблицами виртуальных функций, тогда и это станет очевидней.
Я благодарю за ответ, даже спустя время, я пересмотрел этот урок и ваш ответ, но одно из вашего объяснения остаётся не ясным:
ведь эта строчка обязана выполнятся таким образом
после первой операции p исчезает вовсе и говорить что:
" поскольку указатель p, указывает на Parent, то Child будет приведён к Parent" не верно.
Я немного подумал и пришел к выводу что дело здесь
в ->printType(), дело в том что при компиляции printType() должен понимать с каким типом он будет работать, и когда он видит
p->getThis(), который возвращает parent, он не смотрит глубже и не обращает внимание на виртуальность метода. и когда доходит дело до printType() метода он просто вызывает Parent:: printType, который не является виртуальным, а следовательно и результат будет "returned a Parent\n".
Константин Вам все верно расписал, только Вы зачем-то левое выражение привели к типу правого выражения. Для упрощения попробую расписать немного по другому:
ну а затем Parent вызывает свой метод PrintType(); а не функция решает кто ее вызвал.
Вопрос действительно интересный, вставлю свои пять копеек.
Какой метод вызывается при p->getThis() ?
Мы вызовем getThis() предка, но в таблице виртуальных методов для него будем иметь однотипный метод потомка (ведь это экземпляр Child).
Какой тип возвращает p->getThis()?
Сперва нужно ответить на вопрос, какой this мы будем там иметь?
Так как вызываем через родителя, this будет Child* приведенный к Parent* (как если бы мы static_cast<Parent*>(&ch)) сделали.
Далее у него вызывается не виртуальный метод, и соответственно отрабатывает метод родителя.
Если сомневаетесь, можно вывести возвращаемый тип на экран.
Как-то не зашёл данный урок.
1) Зачем, если мы не хотим делать дочерний метод переопределяющим, ставить final, ведь достаточно в этом случае не указывать virtual? Или это не поможет, т.к. virtual достаточно пометить лишь родительский метод, и дочерний сам того не желая станет переопределяющим?
2) Это вообще было трудно:
методы у нас же для объектов, так? А р является указателем (хоть и на объект), что-то у меня "метод указателя" никак в голове не укладывается. Ну и в принципе выкладка не очевидная, я представить себе не могу чтобы такое где-то повторить.
1) Да, если Вы указываете родительский метод как virtual, то методы в дочерних классах станут переопределяющим автоматичеки (хотя для хорошей читаемости и в дочерних классах ставят virtual)
2) Это написано для удобочитаемости, чтобы явно не разыминовывать указатель. Если написать (*(*p).getThis()).printType(), то будет это выглядеть мягко говоря не очень)
> Да, если Вы указываете родительский метод как virtual, то методы в дочерних классах станут переопределяющим автоматичеки (хотя для хорошей читаемости и в дочерних классах ставят virtual)
Лучше ставить в дочерних классах либо override, либо final. И то, и другое может применяться только к методам, которые были объявлены виртуальными в базовом классе + позволяет обнаружить ситуации, когда в базовом классе была изменена сигнатура виртуального метода, а в классе наследнике — нет.
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rh-override
Да, кстати, проверила последний пример. На моем компиляторе получилось
this is Child
this is Child getThis
this is Child
this is Child getThis
То есть указатель на Child к Parent не приводится в строке:
Хотя Child::getThis() и возвращает Child*, но, поскольку родительская часть объекта возвращает Parent*, то возвращаемый Child* преобразовывается в Parent*.
Можно здесь более подробно? Речь идёт об объекте ch? Почему тогда в первом случае (ch.getThis()->printType();) родительская часть не возвращает *Parent?
Потому что в первом случае getThis вызывается на сразу объекте наследника, а там он возвращает Child*.
А во втором случае метод вызывается через указатель на объект предка, а у предка этот метод возвращает Parent*.
Все эти вещи, на самом деле, правильнее объяснять начиная с типизации. Практически нигде этого не делают и люди-новички приходят к этому через кучу головной боли, лет через 5 от начала обучения.