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

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

Ответы

Ответ 1

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

Ответ 2

  • Это значит, что класс не будет унаследован. Из Wikipedia:

    С++ 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 не являются языковыми ключевыми словами. Они являются техническими идентификаторами; они приобретают особый смысл при использовании в этих конкретных контекстах. В любом другом месте они могут быть действительными идентификаторами.

Ответ 3

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

Ответ 4

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

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

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

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

Этот метод известен как Devirtualization. Термин стоит вспомнить.: -)

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

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

Ответ 5

Финал нельзя применять к не виртуальным функциям.

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, тогда он не разрешен.

Поэтому, вероятно, не имеет смысла делать "верхний уровень" foo final, но он может иметь смысл ниже.

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

Ответ 6

Пример использования ключевого слова "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

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

Ответ 8

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

struct Foo
{
   virtual void DoStuff();
}

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

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

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

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

Ответ 9

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

#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;
}