Какова цель ключевого слова final в C ++ 11 для функций?


146

Какова цель finalключевого слова в C ++ 11 для функций? Я понимаю, что это предотвращает переопределение функций производными классами, но если это так, то разве недостаточно объявить ваши finalфункции не виртуальными ? Есть еще кое-что, что мне здесь не хватает?


30
« разве недостаточно объявить« конечные »функции невиртуальными? » Нет, замещающие функции неявно виртуальны независимо от того, используете вы virtualключевое слово или нет.
ildjarn

13
@ildjarn, это неправда, если они не были объявлены как виртуальные в суперклассе, вы не можете унаследовать от класса и преобразовать невиртуальный метод в виртуальный ..
Дэн О

10
@DanO, я думаю, вы не можете переопределить, но вы можете «скрыть» метод таким образом ... что приводит ко многим проблемам, поскольку люди не хотят скрывать методы.
Alex Kremer

16
@DanO: Если бы он не был виртуальным в суперклассе, он не был бы «отменяющим».
ildjarn

2
Опять же, « переопределение » здесь имеет особое значение, которое заключается в придании полиморфного поведения виртуальной функции. В вашем примере funcне виртуальный, поэтому нечего переопределять и, следовательно, нечего отмечать как overrideили final.
ildjarn

Ответы:


131

Что вам не хватает, поскольку 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

129
  • Это необходимо для предотвращения наследования класса. Из Википедии :

    C ++ 11 также добавляет возможность предотвратить наследование от классов или просто предотвратить переопределение методов в производных классах. Это делается с помощью специального идентификатора final. Например:

    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
спасибо, но я забыл упомянуть, что мой вопрос касался использования "final" с методами
lezebulon

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

Вы это редактировали? Я не вижу сообщений "отредактировано x минут назад пользователем lezebulon". Как это случилось? Может быть, вы отредактировали его очень быстро после отправки?
Аарон МакДэйд

5
@Aaron: правки, внесенные в течение пяти минут после публикации, не отражаются в истории изменений.
ildjarn

@Nawaz: почему это не ключевые слова, а просто спецификаторы? Это из-за соображений совместимости означает, что существующий код до C ++ 11 использует final & override для других целей?
Destructor

46

"final" также позволяет оптимизации компилятора обходить косвенный вызов:

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

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

  void Blah( void ) { DoSomething(); }

};

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


30

Нечего добавить к смысловым аспектам «финала».

Но я хотел бы добавить к комментарию Криса Грина, что "final" может стать очень важным методом оптимизации компилятора в не столь отдаленном будущем. Не только в упомянутом им простом случае, но также и для более сложных иерархий реальных классов, которые могут быть «закрыты» с помощью «final», что позволяет компиляторам генерировать более эффективный диспетчерский код, чем при обычном подходе 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

7
Кто-нибудь знает компилятор, который сейчас их использует?
Винсент Фурмонд 02

то же самое, что я хочу сказать.
crazii

8

Final не может применяться к не виртуальным функциям.

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 переопределил бы его. Это может быть нежелательно, и, следовательно, имеет смысл сделать виртуальную функцию окончательной.

Однако возникает вопрос, зачем разрешать final для виртуальных функций. Если у вас глубокая иерархия:

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, но это может иметь смысл ниже.

(Я думаю, что есть место для расширения слов final и override на невиртуальные члены. Однако они будут иметь другое значение.)


спасибо за пример, в этом я не был уверен. Но все же: какой смысл иметь финальную (и виртуальную) функцию? По сути, вы никогда не сможете использовать тот факт, что функция виртуальная, поскольку ее нельзя переопределить
lezebulon

@lezebulon, я свой вопрос редактировал. Но потом я заметил ответ DanO - это хороший четкий ответ на то, что я пытался сказать.
Аарон МакДэйд

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

8

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

// 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 я всегда хотел, чтобы у C ++ была такая языковая функция, как «final», как у 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!
}

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

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


1

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

#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.