Подтвердить что ты не робот

Какая точка окончательной виртуальной функции?

Wikipedia имеет следующий пример в финальном модификаторе С++ 11:

struct Base2 {
    virtual void f() final;
};

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

Я не понимаю смысла введения виртуальной функции и сразу же маркирую ее как final. Это просто плохой пример, или есть еще что-нибудь?

4b9b3361

Ответ 1

Обычно final не будет использоваться в определении базового класса виртуальной функции. final будет использоваться производным классом, который переопределяет функцию, чтобы предотвратить дальнейшие производные типы от дальнейшего переопределения функции. Поскольку переопределяющая функция должна быть виртуальной, это означает, что любой может переопределить эту функцию в дополнительном производном типе. final позволяет указать функцию, которая переопределяет другую, но которая не может быть переопределена.

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

Ответ 2

Мне это вообще не кажется полезным. Я думаю, что это был просто пример, демонстрирующий синтаксис.

Одним из возможных вариантов использования является то, что вы не хотите, чтобы f действительно переопределялось, но вы все равно хотите сгенерировать vtable, но это все еще ужасный способ сделать что-то.

Ответ 3

Для функции, которая должна быть помечена как final, она должна быть virtual, то есть в С++ 11 & sect; 10.3 para. 2:

[...] Для удобства мы говорим, что любая виртуальная функция переопределяет себя.

и пункт 4:

Если виртуальная функция f в каком-либо классе B отмечена окончанием virt-specifier и в классе D, полученном из B функция D:: f переопределяет B:: f, программа плохо сформирована. [...]

i.e., final требуется использовать только с виртуальными функциями (или с классами для блокировки наследования). Таким образом, для примера используется virtual, чтобы он был допустимым кодом С++.

РЕДАКТИРОВАТЬ: Чтобы быть абсолютно ясным: "точка" спросила о проблемах, почему виртуальная среда даже используется. Основной причиной, почему он используется, является (i), потому что код не будет компилироваться иначе, и (ii) почему сделать пример более сложным, используя большее количество классов, когда этого достаточно? Таким образом, в качестве примера используется ровно один класс с виртуальной конечной функцией.

Ответ 4

Я не понимаю смысла введения виртуальной функции и сразу же маркирую ее как final.

Цель этого примера - показать, как работает final, и он делает именно это.

Практическая цель может заключаться в том, чтобы увидеть, как vtable влияет на размер класса.

struct Base2 {
    virtual void f() final;
};
struct Base1 {
};

assert(sizeof(Base2) != sizeof(Base1)); //probably

Base2 может быть просто использован для проверки специфики платформы, и нет смысла переопределять f(), поскольку он там только для целей тестирования, поэтому он помечен как final. Конечно, если вы это делаете, в дизайне что-то не так. Я лично не создавал класс с функцией virtual, чтобы проверить размер vfptr.

Ответ 5

Добавление к хорошим ответам выше - вот известное приложение окончательного (очень вдохновленное Java). Предположим, что мы определяем функцию wait() в базовом классе, и мы хотим только одну реализацию wait() во всех ее потомках. В этом случае мы можем объявить wait() окончательным.

Например:

class Base { 
   public: 
       virtual void wait() final { cout << "I m inside Base::wait()" << endl; }
       void wait_non_final() { cout << "I m inside Base::wait_non_final()" << endl; }
}; 

и вот определение производного класса:

class Derived : public Base {
      public: 
        // assume programmer had no idea there is a function Base::wait() 

        // error: wait is final
        void wait() { cout << "I am inside Derived::wait() \n"; } 
        // that ok    
        void wait_non_final() { cout << "I am inside Derived::wait_non_final(); }

} 

Было бы бесполезно (и не правильно), если wait() была чистой виртуальной функцией. В этом случае компилятор попросит вас определить wait() внутри производного класса. Если вы это сделаете, это даст вам сообщение об ошибке, потому что wait() является окончательным.

Почему конечная функция должна быть виртуальной? (что тоже запутывает) Поскольку (imo) 1) концепция final очень близка к понятию виртуальных функций [виртуальные функции имеют множество реализаций - конечные функции имеют только одну реализацию), 2) легко реализовать окончательный эффект с помощью vtables.

Ответ 6

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

// Removing foo method is not impacting any child class => this compiles
struct NoImpact { virtual void foo() final {} };
struct OK : NoImpact {};

// Removing foo method is impacting a child class => NOK class does not compile
struct ImpactChildClass { virtual void foo() final {} };
struct NOK : ImpactChildClass { void foo() {} };

int main() {}

Ответ 7

Вместо этого:

public:
    virtual void f();

Мне полезно написать это:

public:
    virtual void f() final
        {
        do_f(); // breakpoint here
        }
protected:
    virtual void do_f();

Основная причина заключается в том, что теперь у вас есть одно место для точки останова перед отправкой в ​​любую из потенциально много переопределенных реализаций. К сожалению (IMHO), говоря, что "final" также требует, чтобы вы сказали "виртуальный".

Ответ 8

Я нашел еще один случай, когда для виртуальной функции полезно объявить окончательным. Этот случай является частью Список предупреждений SonarQube. В предупреждающем описании говорится:

Вызов переопределяемой функции-члена из конструктора или деструктора может привести к неожиданному поведению при создании экземпляра подкласса, который переопределяет функцию-член.

Например:
- По контракту конструктор класса подкласса начинается с вызова конструктора родительского класса.
- Конструктор родительского класса вызывает функцию родительского члена, а не ту, которая переопределена в дочернем классе, что запутывает разработчик дочернего класса.
- Он может создавать поведение undefined, если функция-член является чистой виртуальной в родительском классе.

Несоответствующий пример кода

class Parent {
  public:
    Parent() {
      method1();
      method2(); // Noncompliant; confusing because Parent::method2() will always been called even if the method is overridden
    }
    virtual ~Parent() {
      method3(); // Noncompliant; undefined behavior (ex: throws a "pure virtual method called" exception)
    }
  protected:
    void         method1() { /*...*/ }
    virtual void method2() { /*...*/ }
    virtual void method3() = 0; // pure virtual
};

class Child : public Parent {
  public:
    Child() { // leads to a call to Parent::method2(), not Child::method2()
    }
    virtual ~Child() {
      method3(); // Noncompliant; Child::method3() will always be called even if a child class overrides method3
    }
  protected:
    void method2() override { /*...*/ }
    void method3() override { /*...*/ }
};

Соответствующее решение

class Parent {
  public:
    Parent() {
      method1();
      Parent::method2(); // acceptable but poor design
    }
    virtual ~Parent() {
      // call to pure virtual function removed
    }
  protected:
    void         method1() { /*...*/ }
    virtual void method2() { /*...*/ }
    virtual void method3() = 0;
};

class Child : public Parent {
  public:
    Child() {
    }
    virtual ~Child() {
      method3(); // method3() is now final so this is okay
    }
  protected:
    void method2() override { /*...*/ }
    void method3() final    { /*...*/ } // this virtual function is "final"
};