В C ++, когда я должен использовать final в объявлении виртуального метода?

11

Я знаю, что finalключевое слово используется для предотвращения переопределения виртуального метода производными классами. Тем не менее, я не могу найти никакого полезного примера, когда я действительно должен использовать finalключевое слово с virtualметодом. Более того, такое ощущение, что использование finalвиртуальных методов - это неприятный запах, так как это не позволяет программистам расширять класс в будущем.

Мой вопрос следующий:

Есть ли полезный случай, когда я действительно должен использовать finalв virtualобъявлении метода?

metamaker
источник
По тем же причинам, что методы Java сделаны окончательными?
Ордус
В Java все методы являются виртуальными. Последний метод в базовом классе может улучшить производительность. C ++ имеет не виртуальные методы, поэтому это не относится к этому языку. Знаете ли вы другие хорошие случаи? Я хотел бы услышать их. :)
метамейкер
Думаю, я не очень хорош в объяснении вещей - я пытался сказать то же самое, что и Майк Накис.
Ордус

Ответы:

12

Резюме того, что finalделает ключевое слово: допустим, у нас есть базовый класс Aи производный класс B. Функция f()может быть объявлена Aкак virtual, что означает, что класс Bможет ее переопределить. Но тогда класс Bможет пожелать, чтобы любой класс, который является производным от него, Bне мог переопределять его f(). Вот тогда мы должны объявить f()как finalв B. Без finalключевого слова, если мы определим метод как виртуальный, любой производный класс сможет переопределить его. finalИспользуется ключевое слово , чтобы положить конец этой свободы.

Пример того, почему вам может понадобиться такая вещь: предположим, класс Aопределяет виртуальную функцию prepareEnvelope(), а класс Bпереопределяет ее и реализует как последовательность вызовов своих собственных виртуальных методов stuffEnvelope(), lickEnvelope()и sealEnvelope(). Класс Bнамеревается разрешить производным классам переопределять эти виртуальные методы для предоставления своих собственных реализаций, но класс Bне хочет, чтобы какой-либо производный класс переопределял prepareEnvelope()и, таким образом, изменял порядок вещей, лизал, запечатывал или опускал вызов одной из них. Таким образом, в этом случае класс Bобъявляется prepareEnvelope()как окончательный.

Майк Накис
источник
Во-первых, спасибо за ответ, но разве это не то, что я написал в первом предложении моего вопроса? Что мне действительно нужно, так это случай, когда class B might wish that any class which is further derived from B should not be able to override f()? Есть ли в реальном мире случай, когда такой класс B и метод f () существуют и хорошо подходят?
метамейкер
Да, извините, подождите, я сейчас пишу пример.
Майк Накис
@ Metamaker вот так.
Майк Накис
1
Действительно хороший пример, это лучший ответ на данный момент. Спасибо!
метамейкер
1
Тогда спасибо за потраченное время. Я не смог найти хороший пример в интернете, потратил много времени на поиски. Я бы поставил +1 за ответ, но я не могу этого сделать, поскольку у меня низкая репутация. :( Возможно позже.
metamaker
2

С точки зрения дизайна часто бывает полезно пометить вещи как неизменные. Таким же образом constобеспечивает защиту компилятора и указывает, что состояние не должно меняться, finalможет использоваться для указания того, что поведение не должно меняться дальше вниз по иерархии наследования.

пример

Рассмотрим видеоигру, в которой транспортные средства доставляют игрока из одного места в другое. Все транспортные средства должны проверить, чтобы убедиться, что они едут в правильное место до отправления (например, убедитесь, что база в этом месте не уничтожена). Мы можем начать с использования не-виртуального интерфейса (NVI), чтобы гарантировать, что эта проверка выполняется независимо от транспортного средства.

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

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

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

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

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

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

Вот где finalэто полезно с точки зрения дизайна / архитектуры.

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

Вопрос

Из комментариев:

Почему final и virtual будут использоваться одновременно?

Для базового класса в корне иерархии не имеет смысла объявлять функцию как virtualи final. Это кажется мне довольно глупым, поскольку это заставило бы и компилятора, и читателя-человека перепрыгивать ненужные обручи, которых можно избежать, просто избегая virtualоткровенного в таком случае. Однако подклассы наследуют виртуальные функции-члены следующим образом:

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

В этом случае, независимо от того, Bar::fявно ли используется ключевое слово virtual, Bar::fявляется виртуальной функцией. В virtualэтом случае ключевое слово становится необязательным. Так что может иметь смысл Bar::fуказывать как final, даже если это виртуальная функция ( finalможет использоваться только для виртуальных функций).

И некоторые люди могут предпочесть, чтобы стилистически явно указать, что Bar::fявляется виртуальным, вот так:

struct Bar: Foo
{
   virtual void f() final {...}
};

Для меня это несколько избыточно - использовать оба virtualи finalспецификаторы для одной и той же функции в этом контексте (аналогично virtualи override), но в данном случае это вопрос стиля. Некоторые люди могут обнаружить, virtualчто здесь сообщается что-то ценное, во многом как использование externдля объявлений функций с внешней связью (даже если это необязательно, без других квалификаторов связи).


источник
Как вы планируете переопределить privateметод внутри вашего Vehicleкласса? Разве ты не имел в виду protectedвместо этого?
Энди
3
@DavidPacker privateне распространяется на переопределение (немного нелогично ). Спецификаторы public / protected / private для виртуальных функций применяются только к вызывающим, а не к переопределителям, грубо говоря. Производный класс может переопределять виртуальные функции из своего базового класса независимо от его видимости.
@DavidPacker protectedможет иметь немного более интуитивный смысл. Я просто предпочитаю достигать минимальной видимости, когда это возможно. Я думаю, что причина того, что язык разработан таким образом, заключается в том, что в противном случае частные виртуальные функции-члены не будут иметь никакого смысла вне контекста дружбы, поскольку ни один класс, кроме друга, не сможет переопределить их, если спецификаторы доступа имеют значение в контексте переопределять, а не просто звонить.
Я думал, что если он будет установлен в private, он даже не скомпилируется. Например, ни Java, ни C # не позволяют этого. С ++ удивляет меня каждый день. Даже после 10 лет программирования.
Энди
@DavidPacker Меня тоже сбило с толку, когда я впервые столкнулся с этим. Может быть, я должен просто сделать это, protectedчтобы не запутать других. Я закончил тем, что добавил комментарий, по крайней мере, описывающий, как частные виртуальные функции все еще могут быть переопределены.
0
  1. Это позволяет много оптимизаций, потому что во время компиляции может быть известно, какая функция вызывается.

  2. Будьте осторожны, разбрасывая слово «запах кода». «окончательный» не делает невозможным расширение класса. Двойной щелчок на «последнем» слове, удар по пробелу и расширение класса. ОДНАКО final - это отличная документация, в которой разработчик не ожидает, что вы переопределите функцию, и поэтому следующий разработчик должен быть очень осторожным, потому что класс может перестать работать должным образом, если метод final будет переопределен таким образом.

gnasher729
источник
Почему бы finalи virtualкогда-либо использоваться в то же время?
Роберт Харви
1
Это позволяет много оптимизаций, потому что во время компиляции может быть известно, какая функция вызывается. Можете ли вы объяснить, как или предоставить ссылку? ОДНАКО final - это отличная документация, в которой разработчик не ожидает, что вы переопределите функцию, и поэтому следующий разработчик должен быть очень осторожным, потому что класс может перестать работать должным образом, если метод final будет переопределен таким образом. Разве это не плохой запах?
метамейкер
@RobertHarvey ABI стабильность. В любом другом случае, вероятно, должно быть finalи override.
Дедупликатор
@metamaker У меня была та же самая Q, обсуждаемая здесь в комментариях - programmers.stackexchange.com/questions/256778/… - и, как ни странно, я наткнулся на несколько других обсуждений, только с тех пор как спросил! По сути, речь идет о том, может ли оптимизирующий компилятор / компоновщик определить, может ли ссылка / указатель представлять производный класс и, следовательно, нуждается в виртуальном вызове. Если метод (или класс) доказуемо не может быть переопределен сверх собственного статического типа ссылки, он может извращаться: вызывать напрямую. Если есть какое - либо сомнение - как это часто - они должны назвать виртуально. Объявление finalможет действительно помочь им
underscore_d