Коли в C ++, коли я повинен використовувати final у віртуальному оголошенні методу?


11

Я знаю, що finalключове слово використовується для запобігання перебору віртуального методу похідними класами. Однак я не можу знайти жодного корисного прикладу, коли мені дійсно слід використовувати finalключове слово з virtualметодом. Навіть більше, відчувається, що використання finalвіртуальних методів - це неприємний запах, оскільки він забороняє програмістам розширювати клас у майбутньому.

Наступне моє запитання:

Чи є корисний випадок, коли мені дійсно слід використовувати finalв virtualдекларації методу?


З тих же причин, що методи 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 для відповіді, але не можу це зробити, оскільки маю низьку репутацію. :( Можливо пізніше.
мета-виробник

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корисно з дизайнерської / архітектурної точки зору.

Це також корисно з точки зору оптимізатора, оскільки він надає оптимізатору цю дизайнерську інформацію, що дозволяє йому девіартуалізувати віртуальні виклики функцій (усуваючи накладні динамічні диспетчеризації, а нерідко і значно значніше, усуваючи бар'єр для оптимізації між абонентом та викликом).

Питання

З коментарів:

Чому остаточне та віртуальне коли-небудь використовуватимуться одночасно?

Немає сенсу базовий клас в корені ієрархії оголошувати функцію як і, virtualі final. Мені це здається досить нерозумним, оскільки це би змусило і компілятора, і людського читача перестрибувати непотрібні обручі, чого можна уникнути, просто уникнувши virtualпрямого в такому випадку. Однак підкласи успадковують такі функції віртуальних членів:

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

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

У цьому випадку, незалежно від того, Bar::fвикористовує віртуальне ключове слово чи ні , 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не поширюється на перевизначення (трохи протиінтуїтивне ). Загальнодоступні / захищені / приватні специфікатори для віртуальних функцій застосовуються лише до абонентів, а не до переопределителям. Похідний клас може змінювати віртуальні функції від його базового класу незалежно від його видимості.

@DavidPacker protectedможе мати трохи більш інтуїтивний сенс. Я просто вважаю за краще досягти якомога меншої видимості, коли тільки можу. Я думаю, що причина мови розроблена таким чином полягає в тому, що в іншому випадку приватні функції віртуальних членів не матимуть сенсу за межами контексту дружби, оскільки жоден клас, крім друга, не зможе їх замінити, якби специфікатори доступу мали значення в контексті переосмислення, а не просто дзвінки.

Я думав, що встановити його на приватне навіть не складе. Наприклад, ні Java, ні C # цього не дозволяють. C ++ мене щодня дивує. Навіть після 10 років програмування.
Енді

@DavidPacker Це мене також спалохало, коли я вперше зіткнувся з цим. Можливо, я повинен просто зробити це, protectedщоб не плутати інших. Я, нарешті, розміщую коментар, принаймні, описуючи, як приватні віртуальні функції ще можуть бути замінені.

0
  1. Це дає безліч оптимізацій, тому що під час компіляції може бути відомо, яка функція викликається.

  2. Будьте обережні, кидаючи навколо себе слово "кодовий запах". "final" не унеможливлює розширення класу. Двічі клацніть на «остаточному» слові, натисніть на зворотну простір та розширіть клас. HOWEVER final - це відмінна документація, згідно з якою розробник не очікує, що ви перекриєте функцію, і що через це наступний розробник повинен бути дуже обережним, тому що клас може перестати працювати належним чином, якщо остаточний метод перекриє таким чином.


Чому б finalі virtualколи-небудь використовуватись одночасно?
Роберт Харві

1
Це дає безліч оптимізацій, тому що під час компіляції може бути відомо, яка функція викликається. Чи можете ви пояснити, як або надати посилання? HOWEVER final - це відмінна документація, згідно з якою розробник не очікує, що ви перекриєте функцію, і що через це наступний розробник повинен бути дуже обережним, тому що клас може перестати працювати належним чином, якщо остаточний метод перекриє таким чином. Це не поганий запах?
метамейкер

@ RoberHarvey ABI стабільність. У будь-якому іншому випадку, це повинно бути , ймовірно , finalі override.
Дедуплікатор

@metamaker У мене був такий самий Q, обговорюваний у коментарях тут - programmers.stackexchange.com/questions/256778/… - & смішно натрапив на кілька інших дискусій, лише з часу запитання! В основному, мова йде про те, якщо оптимізуючий компілятор / посилання може визначити, чи може посилання / покажчик представляти похідний клас і, таким чином, потрібен віртуальний виклик. Якщо метод (або клас), очевидно, не може бути замінений за межами власного статичного типу посилання, вони можуть девіртуалізувати: викликати безпосередньо. Якщо є якесь - або сумнів - як це часто - вони повинні назвати віртуально. Декларація finalсправді може допомогти їм
підкреслити
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.