Какова цель ключевого слова "final" в C ++ 11 для функций? - PullRequest
124 голосов
/ 11 января 2012

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

Ответы [ 10 ]

115 голосов
/ 11 января 2012

Что вам не хватает, поскольку 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!
};
115 голосов
/ 11 января 2012
  • Это предотвращает наследование класса. От Википедия :

    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
39 голосов
/ 12 июля 2013

"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().

27 голосов
/ 22 октября 2013

Нечего добавить к семантическим аспектам "final".

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

Одним из ключевых недостатков vtables является то, что для любого такого виртуального объекта (при условии 64-битной загрузки на типичном процессоре Intel) один указатель потребляет до 25% (8 из 64 байт) строки кэша.В тех приложениях, которые я люблю писать, это очень больно.(И по моему опыту это аргумент № 1 против C ++ с точки зрения пуристической производительности, т. Е. Программистами на Си.)

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

Этот метод известен как Девиртуализация .Термин, который стоит запомнить.: -)

В недавнем выступлении Андрея Александреску есть отличная речь, которая довольно хорошо объясняет, как можно обойти такие ситуации сегодня и как "окончательный" может быть частью решения подобных дел "автоматически" в будущем (обсуждается сслушатели):

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

7 голосов
/ 13 сентября 2013

Вариант использования ключевого слова '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 */}
};
7 голосов
/ 11 января 2012

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

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

Было бы не очень важно иметь возможность пометить не виртуальный метод как '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, тогда это не разрешено.

Так что, вероятно, не имеет смысла делать 'top-level' foo final, но это может иметь смысл ниже.

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

4 голосов
/ 11 января 2012

Последнее ключевое слово позволяет вам объявить виртуальный метод, переопределить его N раз, а затем указать, что «это больше не может быть переопределено».Было бы полезно ограничить использование вашего производного класса, чтобы вы могли сказать: «Я знаю, что мой суперкласс позволяет вам переопределить это, но если вы хотите получить от меня, вы не можете!».

struct Foo
{
   virtual void DoStuff();
}

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

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

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

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

4 голосов
/ 11 января 2012

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

1 голос
/ 29 августа 2016

Заключительное ключевое слово в 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 голосов
/ 28 ноября 2018

Дополнение к ответу Марио Кнезовича:

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();

Приведенный выше код демонстрирует теорию, но фактически не тестируется на реальных компиляторах. Очень признателен, если кто-нибудь вставит разобранный вывод.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...