Урок №164. Модификаторы override и final

  Юрий  | 

  |

  Обновл. 27 Сен 2021  | 

 73544

 ǀ   13 

Для решения определенных проблем в наследовании в C++11 добавили два специальных модификатора: override и final. Обратите внимание, эти модификаторы не являются ключевыми словами — это обычные модификаторы, которые имеют особое значение в определенных местах использования.

Хотя final используется не часто, override же является фантастическим дополнением, которое вы должны использовать регулярно. На этом уроке мы рассмотрим оба этих модификатора, а также одно исключение из правил, когда тип возврата переопределения может не совпадать с типом возврата виртуальной функции родительского класса.

Модификатор override

Как мы уже знаем из предыдущего урока, виртуальная функция дочернего класса является переопределением, только если совпадают её сигнатура и тип возврата с сигнатурой и типом возврата виртуальной функции родительского класса. А это, в свою очередь, может привести к проблемам, когда функция, которая должна быть переопределением, на самом деле, им не является.

Рассмотрим следующий пример:

Поскольку 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 (после скобок с параметрами). Если метод не переопределяет виртуальную функцию родительского класса, то компилятор выдаст ошибку:

Здесь мы получим две ошибки: первая для B::getName1() и вторая для B::getName2(), так как ни один из этих методов не является переопределением виртуальных функций класса А. Метод B::getName3() является переопределением, поэтому с ним никаких проблем нет.

Использование модификатора override никак не влияет на эффективность или производительность программы, но помогает избежать непреднамеренных ошибок. Следовательно, настоятельно рекомендуется использовать модификатор override для каждого из своих переопределений.

Правило: Используйте модификатор override для каждого из своих переопределений.

Модификатор final


Могут быть случаи, когда вы не хотите, чтобы кто-то мог переопределить виртуальную функцию или наследовать определенный класс. Модификатор final используется именно для этого. Если пользователь пытается переопределить метод или наследовать класс с модификатором final, то компилятор выдаст ошибку.

Указывается final в том же месте, в котором и модификатор override, например:

В этом коде метод B::getName() переопределяет метод A::getName(). Но B::getName() имеет модификатор final, это означает, что любые дальнейшие переопределения этого метода будут вызывать ошибку компиляции. И действительно, C::getName() уже не может переопределить B::getName() — компилятор выдаст ошибку.

В случае, если мы хотим запретить наследование определенного класса, то модификатор final указывается после имени класса:

В этом примере класс B объявлен как final. Таким образом, класс C не может наследовать класс B — компилятор выдаст ошибку.

Ковариантный тип возврата

Есть один случай, когда тип возврата переопределения может не совпадать с типом возврата виртуальной функции родительского класса, но при этом оставаться переопределением. Если типом возврата виртуальной функции является указатель или ссылка на класс, то переопределения могут возвращать указатель или ссылку на свой собственный класс (т.е. вместо родительского класса указывать на дочерний класс). Это называется ковариантным типом возврата. Например:

Результат выполнения программы:

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.


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (216 оценок, среднее: 4,78 из 5)
Загрузка...

Комментариев: 13

  1. Сергей:

    А если я объявлю override дочернюю функцию, то еще более дочерние виртуальные функции автоматически станут override?

  2. Алексей:

    Не понимаю эту строчку в последнем примере.

    Я понимаю почему вызывается Child::getThis(), который имеет тип возврата Сhild*, тогда почему он возвращает Parent*???

    1. Константин:

      Здравствуйте, наверное уже поздно отвечать, но возможно мой ответ будет полезен другим.
      Попробую растолковать эту строчку подробнее:

      p->getThis(), поскольку этот метод виртуальный, то будет вызван «наиболее» дочерний метод, в данном случае это метод в классе Child.
      Таким образом p->getThis() вернёт указатель Child, как и должен.
      Но поскольку указатель p, указывает на Parent, то Child будет приведён к Parent, и далее printType(), будет вызываться уже у Parent.

      P.S. Я также только учу С++, так что возможно накосячил с терминами. 🙂
      P.P.S. Подробнее разберитесь с таблицами виртуальных функций, тогда и это станет очевидней.

      1. Алексей:

        Я благодарю за ответ, даже спустя время, я пересмотрел этот урок и ваш ответ, но одно из вашего объяснения остаётся не ясным:
        ведь эта строчка обязана выполнятся таким образом

        после первой операции p исчезает вовсе и говорить что:
        " поскольку указатель p, указывает на Parent, то Child будет приведён к Parent" не верно.
        Я немного подумал и пришел к выводу что дело здесь
        в ->printType(), дело в том что при компиляции printType() должен понимать с каким типом он будет работать, и когда он видит
        p->getThis(), который возвращает parent, он не смотрит глубже и не обращает внимание на виртуальность метода. и когда доходит дело до printType() метода он просто вызывает Parent:: printType, который не является виртуальным, а следовательно и результат будет "returned a Parent\n".

        1. Евгений:

          Константин Вам все верно расписал, только Вы зачем-то левое выражение привели к типу правого выражения. Для упрощения попробую расписать немного по другому:

          ну а затем Parent вызывает свой метод PrintType(); а не функция решает кто ее вызвал.

        2. Михаил:

          Вопрос действительно интересный, вставлю свои пять копеек.

          Какой метод вызывается при p->getThis() ?
          Мы вызовем getThis() предка, но в таблице виртуальных методов для него будем иметь однотипный метод потомка (ведь это экземпляр Child).

          Какой тип возвращает p->getThis()?
          Сперва нужно ответить на вопрос, какой this мы будем там иметь?
          Так как вызываем через родителя, this будет Child* приведенный к Parent* (как если бы мы static_cast<Parent*>(&ch)) сделали.

          Далее у него вызывается не виртуальный метод, и соответственно отрабатывает метод родителя.

    2. Finchi:

      Если сомневаетесь, можно вывести возвращаемый тип на экран.

  3. Анастасия:

    Как-то не зашёл данный урок.

    1) Зачем, если мы не хотим делать дочерний метод переопределяющим, ставить final, ведь достаточно в этом случае не указывать virtual? Или это не поможет, т.к. virtual достаточно пометить лишь родительский метод, и дочерний сам того не желая станет переопределяющим?

    2) Это вообще было трудно:

    методы у нас же для объектов, так? А р является указателем (хоть и на объект), что-то у меня "метод указателя" никак в голове не укладывается. Ну и в принципе выкладка не очевидная, я представить себе не могу чтобы такое где-то повторить.

    1. Denis:

      1) Да, если Вы указываете родительский метод как virtual, то методы в дочерних классах станут переопределяющим автоматичеки (хотя для хорошей читаемости и в дочерних классах ставят virtual)
      2) Это написано для удобочитаемости, чтобы явно не разыминовывать указатель. Если написать (*(*p).getThis()).printType(), то будет это выглядеть мягко говоря не очень)

      1. Алексей:

        > Да, если Вы указываете родительский метод как virtual, то методы в дочерних классах станут переопределяющим автоматичеки (хотя для хорошей читаемости и в дочерних классах ставят virtual)

        Лучше ставить в дочерних классах либо override, либо final. И то, и другое может применяться только к методам, которые были объявлены виртуальными в базовом классе + позволяет обнаружить ситуации, когда в базовом классе была изменена сигнатура виртуального метода, а в классе наследнике — нет.

        https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rh-override

  4. Оксана:

    Да, кстати, проверила последний пример. На моем компиляторе получилось

    this is Child
    this is Child getThis
    this is Child
    this is Child getThis

    То есть указатель на Child к Parent не приводится в строке:

  5. Александр:

    Хотя Child::getThis() и возвращает Child*, но, поскольку родительская часть объекта возвращает Parent*, то возвращаемый Child* преобразовывается в Parent*.

    Можно здесь более подробно? Речь идёт об объекте ch? Почему тогда в первом случае (ch.getThis()->printType();) родительская часть не возвращает *Parent?

    1. Петр:

      Потому что в первом случае getThis вызывается на сразу объекте наследника, а там он возвращает Child*.
      А во втором случае метод вызывается через указатель на объект предка, а у предка этот метод возвращает Parent*.
      Все эти вещи, на самом деле, правильнее объяснять начиная с типизации. Практически нигде этого не делают и люди-новички приходят к этому через кучу головной боли, лет через 5 от начала обучения.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *