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

В В Википедии есть следующий пример модификатора final C++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. Это просто плохой пример или что-то еще?


person fredoverflow    schedule 28.07.2012    source источник
comment
Что ж, у Java это есть, так что вы знаете, у C++ тоже должно быть это.   -  person chris    schedule 29.07.2012
comment
Я думаю, это просто плохой пример   -  person Man of One Way    schedule 29.07.2012
comment
Я согласен. Имеет смысл отредактировать статью в Википедии.   -  person Kirill Kobelev    schedule 29.07.2012
comment
Забавный факт: (почти) такой же пример можно найти в стандарте под §10.3/4.   -  person Xeo    schedule 29.07.2012
comment
Вы всегда можете использовать его, чтобы запутать людей, используя ключевое слово в качестве идентификатора: int final = 7; Однако, если вы хотите поговорить об этом со Страуструпом, см. здесь.   -  person chris    schedule 29.07.2012
comment
Это необходимо для того, чтобы код был действительным кодом C++. Если вы удалите виртуальный код из своего кода, то это недопустимая программа на C++. (См. мой ответ ниже.)   -  person Paul Preney    schedule 29.07.2012
comment
@Kirill Kobelev: Они должны отклонить ваш запрос на редактирование, так как это недопустимый код C++.   -  person Paul Preney    schedule 29.07.2012
comment
@PaulPreney, как вы можете решить, что редактирование будет отклонено, не увидев его сначала?   -  person Kirill Kobelev    schedule 29.07.2012
comment
Какая часть этого примера сложна для понимания? Большинство других фрагментов кода примера также бессмысленны. Цель примера — показать, как работает функциональность.   -  person Nicol Bolas    schedule 29.07.2012
comment
@Kirill Kobelev: Очевидно, что никто не может решить, не видя этого, но если бы он просто удалил виртуальный, то это больше не был бы действительным кодом C ++. Ваш комментарий к редактированию записи в Википедии, похоже, подразумевал такое.   -  person Paul Preney    schedule 29.07.2012


Ответы (10)


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

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

person bames53    schedule 28.07.2012
comment
Разве не имело бы тогда смысла, если бы final неявно означало override final? - person fredoverflow; 29.07.2012
comment
Я не вижу причин, почему бы и нет, но и не вижу веских причин делать это, так как вы уже можете помечать функции final override. Возможно, должно быть предупреждение о стиле, которое требует, чтобы все функции final также были помечены override, как и другие предупреждения стиля virtual и override. - person bames53; 29.07.2012
comment
Ну, окончательные методы в интерфейсах были бы примером окончательных методов в базовом классе. Также 'final' выдает ошибку или, по крайней мере, предупреждение, если кто-то определяет функцию с таким же именем в подклассе. Вопрос в том, сразу ли компиляторы девиртуализируют такой случай. - person Trass3r; 11.03.2013
comment
@ Trass3r Было бы невозможно реализовать метод интерфейса final, поскольку реализация метода интерфейса в C++ требует переопределения. Такой нереализуемый интерфейс не был бы полезен. - person bames53; 12.03.2013
comment
Да, именно поэтому вы реализуете это в интерфейсе. - person Trass3r; 12.03.2013
comment
@ Trass3r А, я понимаю, что ты говоришь. Однако я не думаю, что это работает очень хорошо; это не предотвращает скрытие, потому что вы все еще можете объявлять функции с тем же именем, но с разными сигнатурами в производных классах (и такие функции могут использовать аргументы по умолчанию, чтобы иметь одну и ту же сигнатуру, и обычно в любом случае есть предупреждение о сокрытии), и, во-вторых, вы можете переопределять только виртуальные функции, поэтому простое отсутствие отметки виртуальной базовой функции предотвратило бы переопределение. Наконец, использование virtual таким образом кажется мне запутанным. - person bames53; 12.03.2013
comment
@bames53: я сомневался в вашем комментарии о том, что функции, использующие аргументы по умолчанию, имеют одинаковую сигнатуру, но вы правы . Кроме того, виртуальный требуется для использования final. - person idbrii; 08.07.2014

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

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

person Antimony    schedule 28.07.2012
comment
Окончательный и виртуальный - это два разных аспекта. Это становится актуальным в контексте переопределения и перегрузки. Виртуальный квалификатор подразумевает вывод типа во время выполнения. Не виртуальный подразумевает вывод типа типа компиляции. Когда задействована перегрузка и задействовано продвижение/преобразование типа, невиртуальный тип может привести к интересным результатам. Как правило, вам рекомендуется не писать такой код. - person VSOverFlow; 30.07.2012
comment
Зачем вам создавать vtable, если компилятор этого не делает? - person sasha.sochka; 12.08.2013
comment
@sasha.sochka Я думаю, например, что dynamic_cast не будет работать без него. Но обычный способ обеспечить виртуальную таблицу — сделать деструктор виртуальным. - person Mark Ransom; 05.05.2014

Чтобы функция была помечена final, она должна быть virtual, т. е. в С++ 11 10.3 абз. 2:

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

и пункт 4:

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

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

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

person Paul Preney    schedule 28.07.2012
comment
С уважением, я полностью понял суть вопроса. Пункт спросил о проблемах, почему виртуальный вообще используется. Основная причина, по которой он используется, заключается в том, что в противном случае код не будет компилироваться И зачем усложнять пример, используя больше классов, когда достаточно одного? Таким образом, в качестве примера используется ровно один класс с виртуальной конечной функцией. КЭД. - person Paul Preney; 29.07.2012
comment
@Luchian Grigore: я добавил правку, чтобы быть полностью ясным, поскольку я не завершил свой ответ основной причиной. Возможно, почему этот вопрос получает так много комментариев из-за того, что все, естественно, смотрят на пример и говорят: «Зачем кому-то использовать этот код?» и не смотреть на это с точки зрения автора примера. Большинство примеров написано, чтобы быть практически полезными - этот пример не является исключением, чтобы сделать минимальный пример, показывающий, как он себя ведет. - person Paul Preney; 29.07.2012

Не понимаю смысла вводить виртуальную функцию и сразу помечать ее как final.

Цель этого примера — проиллюстрировать, как работает final, и он делает именно это.

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

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

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

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

person Luchian Grigore    schedule 28.07.2012

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

// 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() {}
person Richard Dally    schedule 19.01.2017

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

Например:

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's ok    
        void wait_non_final() { cout << "I am inside Derived::wait_non_final(); }

} 

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

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

person AJed    schedule 28.03.2014
comment
Этот ответ на самом деле объясняет все, а не просто заявляет, что в противном случае это было бы неправильно или потому, что это пример, имхо, это следует принять как ответ на этот вопрос. - person Troyseph; 18.02.2015

Вот почему вы можете объявить функцию как virtual, так и final в базовом классе:

class A {
    void f();
};

class B : public A {
    void f(); // Compiles fine!
};

class C {
    virtual void f() final;
};

class D : public C {
    void f(); // Generates error.
};

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

person Omnifarious    schedule 26.07.2018

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

public:
    virtual void f();

Я считаю полезным написать это:

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

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

person Kevin Hopps    schedule 27.09.2016

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

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

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

Пример несовместимого кода

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"
};
person dismine    schedule 31.03.2017

virtual + final используются в одном объявлении функции, чтобы сделать пример коротким.

Что касается синтаксиса virtual и final, пример из Википедии будет более выразительным, если ввести struct Base2 : Base1 с Base1, содержащей virtual void f();, и Base2, содержащей void f() final; (см. ниже).

Стандарт

Ссылаясь на N3690:

  • virtual, так как function-specifier может быть частью decl-specifier-seq
  • final может быть частью virt-specifier-seq

Не существует правила, согласно которому необходимо использовать вместе ключевое слово virtual и идентификаторы со специальным значением final. Раздел 8.4, определения функций (обратите внимание, опция = необязательно):

определение функции:

атрибут-спецификатор-seq(opt) decl-specifier-seq(opt) декларатор virt-specifier-seq(opt) функция-тело

Упражняться

В C++11 вы можете опустить ключевое слово virtual при использовании final. Это компилируется в gcc > 4.7.1, в clang > 3.0 с C++ 11, в msvc, ... (см. обозреватель компилятора).

struct A
{
    virtual void f() {}
};

struct B : A
{
    void f() final {}
};

int main()
{
    auto b = B();
    b.f();
}

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

PPS: То же самое относится и к override.

person Roi Danton    schedule 10.05.2018