Яке призначення "остаточного" ключового слова в C ++ 11 для функцій?


143

Яке призначення finalключового слова в C ++ 11 для функцій? Я розумію, що це перешкоджає переопределенню функцій похідними класами, але якщо це так, то хіба недостатньо оголосити ваші finalфункції невіртуальними ? Чи є інша річ, якої мені тут не вистачає?


30
" Хіба недостатньо, щоб оголосити вашими" остаточними "функціями " невіртуальними "Ні. Перевіряючі функції явно віртуальними, використовуєте ви virtualключове слово чи ні.
ildjarn

13
@ildjarn це неправда, якщо вони не були оголошені віртуальними в суперкласі, ви не можете вийти з класу і перетворити невіртуальний метод у віртуальний ..
Dan O

10
@DanO Я думаю, що ви не можете перекрити, але ви можете "приховати" метод таким чином .. що призводить до багатьох проблем, оскільки люди не хочуть приховувати методи.
Алекс Кремер

16
@DanO: Якщо це не віртуально в суперкласі, то це не було б "переосмисленням".
ildjarn

2
Знову ж таки, " переосмислення " має тут специфічне значення, яке полягає у наданні поліморфної поведінки віртуальній функції. У вашому прикладі funcне віртуально, тому немає чого переосмислювати, і, отже, нічого не можна позначати як overrideабо final.
ildjarn

Відповіді:


129

Що вам не вистачає, так як idljarn вже згадувалося в коментарях, що якщо ви перевизначення функції з базового класу, то ви не можете , можливо , позначити його як невіртуальну:

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};

Дякую! це те, чого я бракував, тобто, що навіть ваші "листові" класи повинні позначати свою функцію як віртуальну, навіть якщо вони мають намір переосмислити функції, а не перекривати себе
lezebulon

8
@lezebulon: Вашим класам листя не потрібно позначати функцію як віртуальну, якщо суперклас оголосив її як віртуальну.
Dan O

5
Методи в листових класах неявно віртуальні, якщо вони є віртуальними в базовому класі. Я думаю, що компілятори повинні попередити, якщо ця неявна «віртуальна» відсутня.
Аарон Мак-Дейд

@AaronMcDaid: Компілятори зазвичай попереджають про код, який, будучи правильним, може спричинити плутанину чи помилки. Я ніколи не бачив, щоб когось здивувала ця особливість мови таким чином, що могла б викликати будь-які проблеми, тому я не знаю, наскільки корисною може бути ця помилка. Навпаки, забудьте про те, що virtualможе призвести до помилок, і C ++ 11 додав overrideтег до функції, яка виявить таку ситуацію і не зможе скомпілюватись, коли функція, яка призначена для переосмислення, насправді ховається
Девід Родрігес - dribeas

1
З GCC 4.9 відзначає зміни: "Модуль аналізу успадковування нового типу, що покращує девіартуалізацію. Девіртуалізація тепер враховує анонімні простори імен та кінцеве ключове слово C ++ 11" - тому це не лише синтаксичний цукор, але й потенційна користь від оптимізації.
kfsone

127
  • Це запобігти успадкуванню класу. З Вікіпедії :

    C ++ 11 також додає можливість запобігання успадкування від класів або просто запобігання переосмислення методів у похідних класах. Це робиться за допомогою фіналу спеціального ідентифікатора. Наприклад:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
    
  • Він також використовується для позначення віртуальної функції, щоб не допустити її перекриття у похідних класах:

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };
    

Далі Вікіпедія робить цікавий момент :

Зауважте, що ні мовні ключові слова overrideні finalВони є технічними ідентифікаторами; особливого значення вони набувають лише тоді, коли використовуються в цих конкретних контекстах . У будь-якому іншому місці вони можуть бути дійсними ідентифікаторами.

Це означає, що дозволено:

int const final = 0;     // ok
int const override = 1;  // ok

1
дякую, але я забув зазначити, що моє запитання стосувалося використання "остаточного" методів
lezebulon

Ви згадали про це @lezebulon :-) "яка мета" остаточного "ключового слова в C ++ 11 для функцій ". (мій наголос)
Аарон Мак-Дейд

Ви це відредагували? Я не бачу жодного повідомлення з повідомленням "відредаговано x хвилин тому від lezebulon". Як це сталося? Можливо, ви дуже швидко відредагували його після надсилання?
Аарон Мак-Дейд

5
@Aaron: Зміни, зроблені протягом п'яти хвилин після публікації, не відображаються в історії редагування.
ildjarn

@Nawaz: чому вони не ключові слова, а лише специфікатори? Це через причини сумісності означає, що можливо, що попередньо існуючий код перед C ++ 11 використовує остаточне та переопределене для інших цілей?
Деструктор

45

"final" також дозволяє оптимізацію компілятора обійти непрямим викликом:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

при "остаточному" компілятор може дзвонити CDerived::DoSomething()безпосередньо зсередини Blah()або навіть вбудований. Без цього він повинен генерувати непрямий виклик всередині, Blah()оскільки його Blah()можна було б викликати всередині похідного класу, який перекрив DoSomething().


29

Нічого не додасть до семантичних аспектів "фіналу".

Але я хотів би додати до коментаря Кріса Гріна, що "фінал" може стати дуже важливою технікою оптимізації компілятора в не так далекому майбутньому. Не тільки в простому випадку, про який він згадував, але і для більш складних ієрархій реального класу, які можна "закрити" "остаточним", що дозволяє компіляторам генерувати більш ефективний диспетчерський код, ніж при звичайному vtable-підході.

Одним з основних недоліків vtables є те, що для будь-якого такого віртуального об'єкта (якщо вважати 64-бітовим на типовому процесорі Intel) вказівник сам з'їдає 25% (8 з 64 байт) лінії кешу. У видах додатків, які мені подобається писати, це дуже боляче. (І з мого досвіду, це аргумент №1 проти C ++ з точки зору пуристичної продуктивності, тобто програмістів на C.)

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

Ця методика відома як Девіртуалізація . Термін, який варто пам’ятати. :-)

Нещодавно виступив Андрій Олександреску, який досить добре пояснює, як можна вирішити подібні ситуації сьогодні та як "остаточний" може бути частиною автоматичного вирішення подібних справ у майбутньому (обговорюється із слухачами):

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly


23
8 - це 25% від 64?
ildjarn

6
Хтось знає про компілятор, який використовує ці зараз?
Вінсент Фурмонд

те саме, що я хочу сказати.
crazii

8

Заключний не може бути застосований до невіртуальних функцій.

error: only virtual member functions can be marked 'final'

Було б не дуже важливим, щоб можна було позначити невіртуальний метод як "остаточний". Дано

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()завжди дзвонить A::foo.

Але, якби A :: foo був virtual, то B :: foo змінив би його. Це може бути небажаним, і, отже, було б сенсом зробити віртуальну функцію остаточною.

Питання, однак, чому дозволити остаточні для віртуальних функцій. Якщо у вас є глибока ієрархія:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

Тоді finalставиться «підлогу» щодо того, наскільки важливим можна зробити. Інші класи можуть поширювати A і B і змінювати їх foo, але це клас розширює C, тоді це не дозволено.

Тому, мабуть, не має сенсу робити "верхній рівень" foo final, але це може мати сенс нижче.

(Я думаю, що все ж є можливість поширити слова остаточні та замінити на невіртуальних членів. Хоча вони мали б інше значення.)


дякую за приклад, це щось, в чому я не був впевнений. Але все-таки: який сенс мати остаточну (і віртуальну) функцію? В основному ви ніколи не зможете використовувати той факт, що функція є віртуальною, оскільки її неможливо переосмислити
lezebulon

@lezebulon, я змінив своє запитання. Але потім я помітив відповідь DanO - це гарна чітка відповідь на те, що я намагався сказати.
Аарон Мак-Дейд

Я не експерт, але я вважаю, що іноді може бути сенс зробити функцію вищого рівня final. Наприклад, якщо ви знаєте, що ви хочете, щоб всі Shapeбули - foo()щось заздалегідь визначене і визначене, що жодна похідна форма не повинна змінюватись. Або я помиляюсь, і для цього випадку є краща модель? РЕДАКТ: О, може, тому що в такому випадку просто не варто робити так, щоб вищий рівень foo() virtualпочинався з цього? Але все-таки це можна приховати, навіть якщо його правильно (поліморфно) зателефонувати через Shape*...
Ендрю Чеонг

8

Випадок використання "остаточного" ключового слова, яке мені подобається, полягає в наступному:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};

1
Так, це по суті приклад шаблону методу шаблону. І раніше, ніж C ++ 11, мені завжди хотілося, щоб TMP бажав, щоб у C ++ була така мовна особливість, як "остаточний", як це робила Java.
Кайтайн

6

final додає явний намір не перекривати вашу функцію і призведе до помилки компілятора, якщо це буде порушено:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

Як код стоїть, він компілює і B::fooпереосмислює A::foo. B::fooтакож віртуальна, до речі. Однак якщо ми змінимо №1 на virtual int foo() final, це помилка компілятора, і нам не дозволяється більше змінювати перехід A::fooу похідних класах.

Зауважте, що це не дозволяє нам "знову відкрити" нову ієрархію, тобто немає способу зробити B::fooнову, неспоріднену функцію, яка може бути незалежно на чолі нової віртуальної ієрархії. Після того, як функція остаточна, вона ніколи не може бути оголошена знову в жодному похідному класі.


5

Заключне ключове слово дозволяє оголосити віртуальний метод, переосмислити його N разів, а потім призначити, що "це більше не можна перекривати". Було б корисно обмежити використання похідного класу, так що ви можете сказати "Я знаю, що мій суперклас дозволяє вам переоцінити це, але якщо ви хочете отримати від мене, ви не можете!".

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

Як вказували інші афіші, його не можна застосувати до невіртуальних функцій.

Однією метою остаточного ключового слова є запобігання випадкового перезазначення методу. У моєму прикладі DoStuff (), можливо, була допоміжною функцією, яку похідному класу просто потрібно перейменувати, щоб отримати правильну поведінку. Без остаточної помилки не було б виявлено до тестування.


1

Заключне ключове слово в C ++, додане до функції, не дозволяє йому перекрити базовий клас. Також при додаванні до класу запобігає успадкуванню будь-якого типу. Розглянемо наступний приклад, який показує використання кінцевого специфікатора. Ця програма не працює в компіляції.

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

Також:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}

0

Доповнення до відповіді Маріо Кнезович:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

Наведений вище код показує теорію, але насправді не перевіряється на реальних компіляторах. Багато вдячний, якщо хтось вставить розібраний вихід.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.