С++ "виртуальное" ключевое слово для функций в производных классах. Это необходимо?

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

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

Подход №1:

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

Подход № 2:

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

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

Ответ 1

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

Ответ 2

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

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

Ответ 3

Ключевое слово virtual не требуется в производном классе. Здесь подтверждающая документация, начиная с С++ Draft Standard (N3337) (выделение):

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

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

Ответ 4

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

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

Однако эта ловушка, к счастью, адресована языковой функцией С++ 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: С++ 11.

Ответ 5

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

Ответ 6

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

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

Ответ 7

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

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 что-то сделать, если хотите.

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

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

Ответ 8

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

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

Ответ 9

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