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

221

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

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

Подход № 1:

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

Подход № 2:

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

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

Anarki
источник
65
В C ++ 11 вы можете написать «void hello () override {}», чтобы явно объявить, что вы переопределяете виртуальный метод. Компилятор потерпит неудачу, если базовый виртуальный метод не существует, и он будет иметь такую ​​же читаемость, как и размещение «virtual» в классе-потомке.
ShadowChaser
На самом деле, в gcc C ++ 11 написание void hello () override {} в производном классе хорошо, потому что базовый класс указал, что метод hello () является виртуальным. Другими словами, использование слова virtual в производном классе не обязательно / не обязательно, в любом случае для gcc / g ++. (Я использую gcc версии 4.9.2 на RPi 3) Но в любом случае рекомендуется включать ключевое слово virtual в метод производного класса.
Будет ли

Ответы:

183

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

Джеймс МакНеллис
источник
25
Это правда, но Руководство по переносимости Mozilla C ++ рекомендует всегда использовать виртуальный, потому что «некоторые компиляторы» выдают предупреждения, если вы этого не сделаете. Жаль, что они не упоминают никаких примеров таких компиляторов.
Сергей Таченов
5
Я также хотел бы добавить, что явное пометив его как виртуальный, напомнит вам о необходимости сделать виртуальный деструктор.
Ифалин
1
Только для того, чтобы упомянуть, то же самое относится к виртуальному деструктору
Атул
6
@SergeyTachenov согласно Clifford комментария «s к своему ответу , например , таких компиляторов ARMCC.
Руслан
4
@Rasmi, новое руководство по переносимости здесь , но теперь оно рекомендует использовать overrideключевое слово.
Сергей Таченов
83

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

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

Клиффорд
источник
13
Какой это компилятор?
Джеймс МакНеллис
35
@James: armcc (компилятор ARM для устройств ARM)
Клиффорд,
55

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

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

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

Р Саху
источник
5
Это, безусловно, лучший ответ здесь.
Фантастический мистер Фокс
33

Нет, 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аргумент a ).

Обратитесь к WP: C ++ 11 .

Колин Д Беннетт
источник
11

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

Суджай Гош
источник
7

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

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

арфист
источник
2

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

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

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

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

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

lorro
источник
1

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

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

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