18.3 – Спецификаторы override и final, и ковариантные возвращаемые типы

Добавлено 18 августа 2021 в 00:14

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

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

Спецификатор override

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

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

#include <iostream>
#include <string_view>
 
class A
{
public:
    virtual std::string_view getName1(int x) { return "A"; }
    virtual std::string_view getName2(int x) { return "A"; }
};
 
class B : public A
{
public:
    // обратите внимание: параметр имеет тип short int
    virtual std::string_view getName1(short int x) { return "B"; } 
    // обратите внимание: функция - константная
    virtual std::string_view getName2(int x) const { return "B"; } 
};
 
int main()
{
    B b{};
    A& rBase{ b };
    std::cout << rBase.getName1(1) << '\n';
    std::cout << rBase.getName2(2) << '\n';
 
    return 0;
}

Поскольку rBase является ссылкой типа A на объект B, предполагалось, чтобы здесь использовались виртуальные функции доступа к B::getName1() и B::getName2(). Однако, поскольку B::getName1() принимает другой параметр (short int вместо int), она не считается переопределением A::getName1(). Более коварно то, что, поскольку B::getName2() является константной, а A::getName2() – нет, B::getName2() не считается переопределением A::getName2().

Следовательно, эта программа печатает:

A
A

В этом конкретном случае, поскольку A и B просто печатают свои имена, довольно легко увидеть, что мы испортили наши переопределения и вызывается неправильная виртуальная функция. Однако в более сложной программе, где функции имеют поведение или возвращаемые значения, которые не выводятся на печать, такие ошибки может быть очень трудно отладить.

Чтобы помочь решить проблему функций, которые предназначены для переопределения, но не являются переопределением, к любой виртуальной функции можно применить спецификатор override, поместив его в то же место, где должен был бы находиться спецификатор const. Если функция не переопределяет функцию базового класса (или применяется к невиртуальной функции), компилятор пометит функцию как ошибку.

#include <string_view>
 
class A
{
public:
    virtual std::string_view getName1(int x) { return "A"; }
    virtual std::string_view getName2(int x) { return "A"; }
    virtual std::string_view getName3(int x) { return "A"; }
};
 
class B : public A
{
public:
    // ошибка компиляции, функция не является переопределением
    std::string_view getName1(short int x) override { return "B"; }
    // ошибка компиляции, функция не является переопределением
    std::string_view getName2(int x) const override { return "B"; }
    // ok, функция является переопределением A::getName3(int)
    std::string_view getName3(int x) override { return "B"; } 
 
};
 
int main()
{
    return 0;
}

Приведенная выше программа вызывает две ошибки компиляции: одну для B::getName1() и одну для B::getName2(), потому что ни одна из них не переопределяет предыдущую функцию. B::getName3() переопределяет A::getName3(), поэтому для этой строки не возникает ошибок.

Мы можем удалить ключевое слово virtual, потому что спецификатор override подразумевает виртуальность. Использование и virtual, и override было бы излишним.

Лучшая практика


Отмечайте виртуальные функции или как virtual, или как override, но не то и другое одновременно.

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

Лучшая практика


Применяйте спецификатор override к каждой, предназначенной переопределения функции, которую вы пишете.

Спецификатор final

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

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

#include <string_view>
 
class A
{
public:
    virtual std::string_view getName() { return "A"; }
};
 
class B : public A
{
public:
    // обратите внимание на использование спецификатора final в следующей строке
    // - это делает эту функцию больше не переопределяемой
    // ok, переопределяет A::getName()
    std::string_view getName() override final { return "B"; } 
};
 
class C : public B
{
public:
    // ошибка компиляции: переопределяет B::getName(),
    // которая объявлена конечной
    std::string_view getName() override { return "C"; }
};

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

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

#include <string_view>
 
class A
{
public:
    virtual std::string_view getName() { return "A"; }
};
 
class B final : public A // обратите внимание на спецификатор final
{
public:
    std::string_view getName() override { return "B"; }
};
 
class C : public B // ошибка компиляции: нельзя наследоваться от конечного класса
{
public:
    std::string_view getName() override { return "C"; }
};

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

Ковариантные возвращаемые типы

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

#include <iostream>
#include <string_view>
 
class Base
{
public:
    // Эта версия getThis() возвращает указатель класса Base
    virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }
    void printType() { std::cout << "returned a Base\n"; }
};
 
class Derived : public Base
{
public:
    // Обычно переопределяющие функции должны возвращать объекты того же типа,
    // что и базовая функция.
    // Однако, поскольку Derived является производным от Base,
    // можно вернуть Derived* вместо Base*
    Derived* getThis() override { std::cout << "called Derived::getThis()\n";  return this; }
    void printType() { std::cout << "returned a Derived\n"; }
};
 
int main()
{
    Derived d{};
    Base* b{ &d };
    // вызывает Derived::getThis(), возвращающую Derived*, вызывает Derived::printType
    d.getThis()->printType(); 
    // вызывает Derived::getThis(), возвращающую Base*, вызывает Base::printType
    b->getThis()->printType(); 
 
    return 0;
}

Эта программа печатает:

called Derived::getThis()
returned a Derived
called Derived::getThis()
returned a Base

Одно интересное замечание о ковариантных возвращаемых типах: C++ не может динамически выбирать типы, поэтому вы всегда будете получать тип, соответствующий базовой версии вызываемой функции.

В приведенном выше примере мы сначала вызываем d.getThis(). Поскольку d является объектом производного класса, это вызывает Derived::getThis(), которая возвращает Derived*. Этот Derived* затем используется для вызова невиртуальной функции Derived::printType().

А теперь интересный случай. Затем мы вызываем b->getThis(). Переменная b – это указатель типа Base на объект производного класса. Base::getThis() – виртуальная функция, поэтому она вызывает Derived::getThis(). Хотя Derived::getThis() возвращает Derived*, поскольку базовая версия этой функции возвращает Base*, возвращаемый Derived* преобразуется в Base*. И поэтому вызывается Base::printType().

Другими словами, в приведенном выше примере вы получите Derived* только в том случае, если вызовете getThis() с объектом, который изначально набран как объект Derived.

Теги

C++ / CppfinalLearnCppoverrideВиртуальная функцияДля начинающихКласс (программирование)НаследованиеОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.