C ++ «виртуальное» ключевое слово для функций в производных классах. Это необходимо? - PullRequest
205 голосов
/ 04 февраля 2011

С определением структуры, приведенным ниже ...

struct A {
    virtual void hello() = 0;
};

Подход № 1:

struct B : public A {
    virtual void hello() { ... }
};

Подход № 2:

struct B : public A {
    void hello() { ... }
};

Есть ли разница между этими двумя способами переопределения функции hello?

Ответы [ 9 ]

165 голосов
/ 04 февраля 2011

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

80 голосов
/ 04 февраля 2011

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

С чисто стилистической точки зрения включение ключевого слова virtual четко «рекламирует» тот факт, что функция является виртуальной. Это будет важно для любого дальнейшего подкласса B без необходимости проверять определение A. Для глубокой иерархии классов это становится особенно важным.

47 голосов
/ 21 августа 2014

Ключевое слово virtual не является обязательным в производном классе.Вот вспомогательная документация из чернового стандарта C ++ (N3337) (выделено мной):

10.3 Виртуальные функции

2 Если функция виртуального члена vf объявлено в классе Base и в классе Derived, полученном прямо или косвенно из Base, функции-члена vf с тем же именем, список параметров типа (8.3.5), cv-объявляется квалификация и ref-квалификатор (или его отсутствие) как Base::vf, тогда Derived::vf также является виртуальным ( независимо от того, объявлено ли оно ) и переопределяет Base::vf.

31 голосов
/ 15 октября 2014

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

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

Однако, к счастью, это ловушкаадресовано функцией языка C ++ 11 явное переопределение , которая позволяет исходному коду четко указывать, что функция-член предназначена для переопределения функции базового класса:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

Компиляторвыдаст ошибку времени компиляции, и ошибка программирования будет сразу же очевидна (возможно, функция в Derived должна была принять float в качестве аргумента).

См. WP: C ++ 11.

10 голосов
/ 04 февраля 2011

Добавление «виртуального» ключевого слова является хорошей практикой, поскольку оно улучшает читабельность, но не является обязательным. Функции, объявленные виртуальными в базовом классе и имеющие одинаковую подпись в производных классах, по умолчанию считаются «виртуальными».

7 голосов
/ 04 февраля 2011

Нет никакой разницы для компилятора, когда вы пишете virtual в производном классе или пропускаете его.

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

1 голос
/ 28 июля 2016

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

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

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

Обратите внимание, что если вы сделаете это, вы можете также объявить конструкторы копирования / перемещения в качестве шаблонов: разрешение конструировать из различных интерфейсов позволяет вам «приводить» между различными типами B<>.

Сомнительно, стоит ли добавлять поддержку const A& в t_hello(). Обычная причина такого переписывания - перейти от специализации на основе наследования к специализации на основе шаблонов, в основном по соображениям производительности. Если вы продолжаете поддерживать старый интерфейс, вы вряд ли сможете обнаружить (или предотвратить) старое использование.

0 голосов
/ 08 декабря 2018

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

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Здесь Cнаследуется от B, поэтому B не является базовым классом (это также производный класс), а C является производным классом.Диаграмма наследования выглядит следующим образом:

A
^
|
B
^
|
C

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

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

0 голосов
/ 16 марта 2014

Я обязательно включу ключевое слово Virtual для дочернего класса, потому что

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