Ответ 1
Они точно такие же. Между ними нет никакой разницы, кроме того, что первый подход требует большего набора текста и потенциально более ясен.
С приведенным ниже описанием структуры...
struct A {
virtual void hello() = 0;
};
Подход №1:
struct B : public A {
virtual void hello() { ... }
};
Подход № 2:
struct B : public A {
void hello() { ... }
};
Есть ли разница между этими двумя способами переопределения функции hello?
Они точно такие же. Между ними нет никакой разницы, кроме того, что первый подход требует большего набора текста и потенциально более ясен.
"Виртуальность" функции распространяется неявно, однако, по крайней мере, один из компиляторов, которые я использую, генерирует предупреждение, если ключевое слово virtual
не используется явно, поэтому вы можете использовать его, если только для сохранения компилятора.
С чисто стилистической точки зрения, в том числе ключевое слово virtual
четко "рекламирует" факт для пользователя, что функция является виртуальной. Это будет важно для любого последующего подкласса B без необходимости проверки определения A. Для глубоких иерархий классов это становится особенно важным.
Ключевое слово 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
.
Нет, ключевое слово 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.
Добавление ключевого слова "virtual" - это хорошая практика, поскольку она улучшает читаемость, но это необязательно. Функции, объявленные виртуальными в базовом классе и имеющие одну и ту же подпись в производных классах, по умолчанию считаются "виртуальными".
Нет никакой разницы для компилятора, когда вы пишете virtual
в производном классе или опустите его.
Но вам нужно посмотреть базовый класс, чтобы получить эту информацию. Поэтому я бы рекомендовал добавить ключевое слово virtual
также в производном классе, если вы хотите показать человеку, что эта функция виртуальна.
Там есть значительная разница, когда у вас есть шаблоны и начинайте брать базовый класс в качестве параметра (-ов) шаблона:
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()
. Обычной причиной этого переписывания является переход от специализированной наследования к основанной на шаблонах, главным образом по соображениям производительности. Если вы продолжаете поддерживать старый интерфейс, вы вряд ли сможете обнаружить (или сдержать) старое использование.
Я обязательно включу ключевое слово Virtual для дочернего класса, потому что
Ключевое слово 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
перед функциями во всех классах, участвующих в наследовании, если только вы точно не знаете, что у класса не будет детей, которым нужно было бы переопределять функции базового класса. Это хорошая практика.