Что делает квалификатор final в C++ и какую дополнительную функциональность он дает
Что проверяют
Знаете ли вы, что final работает только вместе с наследованием и виртуальностью, и понимаете ли, что он даёт компилятору. Заодно проверяют, различаете ли вы final и override.
Как отвечать
final применяется в двух местах. На классе он запрещает от него наследоваться. На виртуальной функции он запрещает переопределять её дальше по иерархии.
struct Base {
virtual void run();
};
struct Middle : Base {
void run() override final;
};
struct Leaf final : Middle {};Скажите две вещи, которые отличают знающий ответ.
Первая: final не ключевое слово, а контекстно-зависимый идентификатор. Переменную с именем final объявить можно, компилятор трактует слово по позиции. То же верно и для override.
Вторая: зачем это на практике. Кроме защиты инварианта («класс не рассчитан на наследование, у него невиртуальный деструктор»), final даёт право на девиртуализацию. Если статический тип объекта помечен final или помечена сама функция, компилятор знает точную реализацию, может вызвать её напрямую и встроить, минуя таблицу виртуальных функций.
Полезно добавить, что final на классе честнее трюков с приватным конструктором: намерение видно в объявлении, а ошибка ловится на этапе компиляции.
Где спотыкаются
Вешают final на невиртуальную функцию и ждут, что это запретит её скрывать в наследнике. Такой код не компилируется: спецификатор допустим только для виртуальной функции, а скрытие имени он не регулирует вообще.
Второе: обещают ускорение от final где угодно. Девиртуализация случается, когда компилятор видит точный тип. При вызове через указатель на базу знание о том, что где-то ниже класс помечен final, ему не поможет, и вызов останется косвенным.
Третье: считают, что final заменяет override. Это разные проверки: override требует, чтобы функция действительно что-то переопределяла, и ловит опечатку в сигнатуре, final запрещает переопределять дальше. Писать их вместе допустимо и полезно.