C ++ "віртуальне" ключове слово для функцій у похідних класах. Це потрібно?


221

Із визначенням структури, наведеним нижче ...

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

Підхід №1:

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

Підхід №2:

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

Чи є різниця між цими двома способами перекриття привітної функції?


65
У C ++ 11 ви можете написати "void hello () override {}", щоб явно заявити, що ви переосмислюєте віртуальний метод. Компілятор вийде з ладу, якщо базовий віртуальний метод не існує, і він має таку ж читабельність, що і розміщення "віртуального" в класі нащадків.
ShadowChaser

Насправді, у C ++ 11 gcc написання void hello () override {} у похідному класі добре, оскільки базовий клас вказав, що метод hello () є віртуальним. Іншими словами, використання слова virtual у похідному класі не є необхідним / обов'язковим, для gcc / g ++ у будь-якому разі. (Я використовую gcc версії 4.9.2 на RPi 3) Але в будь-якому випадку є хорошою практикою включати ключове слово віртуально у метод похідного класу.
Буде чи

Відповіді:


183

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


25
Це правда, але посібник з портативності Mozilla C ++ рекомендує завжди використовувати віртуальні, оскільки "деякі компілятори" видають попередження, якщо ви цього не зробите. Шкода, що вони не згадують жодних прикладів таких компіляторів.
Сергій Таченов

5
Я також додам, що явне позначення його як віртуального допоможе нагадати вам зробити ще й деструктор віртуальним.
lfalin

1
Зазначимо лише те саме, що стосується віртуального деструктора
Atul

6
@SergeyTachenov відповідно до коментаря clifford до власної відповіді , прикладом таких компіляторів є armcc.
Руслан

4
@Rasmi, новий посібник з портативності є тут , але тепер він рекомендує використовувати overrideключове слово.
Сергій Таченов

83

"Віртуальність" функції поширюється неявно, однак принаймні один компілятор, який я використовую, генерує попередження, якщо virtualключове слово не використовується явно, тож ви можете використовувати його, щоб лише утримувати спокій компілятора.

З чисто стилістичної точки зору, включаючи virtualключове слово, чітко «рекламує» факт, що функція є віртуальною. Це буде важливо для будь-якого подальшого підкласу B, не перевіряючи визначення A. Для ієрархій глибоких класів це стає особливо важливим.


12
Який це компілятор?
James McNellis

35
@James: armcc (компілятор ARM для пристроїв ARM)
Кліффорд

55

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

10.3 Віртуальні функції

2 Якщо функція віртуального члена vfоголошена в класі Baseта в класі Derived, похідному прямо чи опосередковано Base, функція-член vfз тим самим іменем, списком типів параметрів (8.3.5), cv-кваліфікацією та ref-kvalifier ( або відсутність того ж), що 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
};

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

Зверніться до 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(). Звичайна причина цього переписування - перехід від спеціалізації, заснованої на спадщині, до шаблону, в основному з міркувань продуктивності. Якщо ви продовжуєте підтримувати старий інтерфейс, ви навряд чи зможете виявити (або відмовитися від) старого використання.


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

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

  • i. Читабельність.
  • ii. Цей дочірній клас мій буде отриманий далі вниз, ви не хочете, щоб конструктор наступного похідного класу викликав цю віртуальну функцію.

1
Я думаю, що він означає, що без позначення дочірньої функції як віртуальної програміст, який пізніше піде з дочірнього класу, може не усвідомити, що функція насправді є віртуальною (тому що він ніколи не дивився на базовий клас) і може потенційно викликати її під час побудови ( що може чи не може зробити правильно).
PfhorSlayer
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.