C ++ „wirtualne” słowo kluczowe dla funkcji w klasach pochodnych. Czy to konieczne?

221

Z podaną poniżej definicją struktury ...

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

Podejście nr 1:

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

Podejście nr 2:

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

Czy jest jakaś różnica między tymi dwoma sposobami zastąpienia funkcji hello?

Anarki
źródło
65
W C ++ 11 możesz napisać „void hello () override {}”), aby jawnie zadeklarować, że przesłaniasz metodę wirtualną. Kompilator zawiedzie, jeśli podstawowa metoda wirtualna nie istnieje, i ma taką samą czytelność jak umieszczanie „wirtualnego” w klasie potomnej.
ShadowChaser
W rzeczywistości w C ++ 11 gcc pisanie void hello () override {} w klasie pochodnej jest w porządku, ponieważ klasa podstawowa określiła, że ​​metoda hello () jest wirtualna. Innymi słowy, użycie słowa virtual w klasie pochodnej nie jest konieczne / obowiązkowe, tak czy inaczej dla gcc / g ++. (Używam gcc w wersji 4.9.2 na RPi 3) Ale dobrą praktyką jest uwzględnienie słowa kluczowego virtual w metodzie klasy pochodnej.
Czy

Odpowiedzi:

183

Są dokładnie takie same. Nie ma między nimi żadnej różnicy poza tym, że pierwsze podejście wymaga więcej pisania i jest potencjalnie jaśniejsze.

James McNellis
źródło
25
To prawda, ale Przewodnik po przenośności Mozilla C ++ zaleca zawsze używanie wirtualnych, ponieważ „niektóre kompilatory” wydają ostrzeżenia, jeśli tak nie jest. Szkoda, że ​​nie wspominają o żadnych przykładach takich kompilatorów.
Siergiej Tachenov
5
Dodałbym również, że jawne oznaczenie go jako wirtualnego pomoże ci przypomnieć, abyś także uczynił wirtualny destruktor wirtualny.
lfalin,
1
Tylko wspomnieć, to samo dotyczy wirtualnego destruktora
Atul
6
@SergeyTachenov według komentarza Clifforda do własnej odpowiedzi , przykładem takich kompilatorów jest armcc.
Ruslan,
4
@Rasmi, nowy przewodnik dotyczący przenoszenia jest już dostępny , ale teraz zaleca użycie overridesłowa kluczowego.
Siergiej Tachenov
83

„Wirtualność” funkcji jest propagowana niejawnie, jednak przynajmniej jeden kompilator, którego używam, wygeneruje ostrzeżenie, jeśli virtualsłowo kluczowe nie zostanie użyte jawnie, więc możesz go użyć, aby zachować kompilator w spokoju.

Z czysto stylistycznego punktu widzenia, w tym virtualsłowa kluczowego wyraźnie „reklamuje” fakt, że funkcja jest wirtualna. Będzie to ważne dla każdej dalszej podklasy B bez konieczności sprawdzania definicji A. Dla głębokich hierarchii klasowych staje się to szczególnie ważne.

Clifford
źródło
12
Który to kompilator?
James McNellis,
35
@James: armcc (kompilator ARM dla urządzeń ARM)
Clifford
55

Słowo virtualkluczowe nie jest konieczne w klasie pochodnej. Oto dokumentacja pomocnicza z C ++ Draft Standard (N3337) (wyróżnienie moje):

10.3 Funkcje wirtualne

2 Jeśli wirtualna funkcja członka vfjest zadeklarowana w klasie Basei klasie Derived, wyprowadzona bezpośrednio lub pośrednio z Base, funkcja członka vfo tej samej nazwie, liście typów parametrów (8.3.5), kwalifikacjach cv i kwalifikatorze odniesienia ( lub brak tego samego), co Base::vfzadeklarowano, to Derived::vfjest również wirtualny ( niezależnie od tego, czy jest tak zadeklarowany ) i zastępuje Base::vf.

R Sahu
źródło
5
To zdecydowanie najlepsza odpowiedź tutaj.
Fantastyczny Pan Lis
33

Nie, virtualsłowo kluczowe w zastępowaniu funkcji wirtualnej klas pochodnych nie jest wymagane. Warto jednak wspomnieć o związanej z tym pułapce: niepowodzeniu zastąpienia funkcji wirtualnej.

Brak ręcznego występuje, jeśli zamierzają zastąpić funkcję wirtualną w klasie pochodnej, ale popełniła błędu w podpisie tak, że deklaruje nową i inną funkcję wirtualną. Ta funkcja może być przeciążeniem funkcji klasy podstawowej lub może różnić się nazwą. Bez względu na to, czy użyjesz virtualsłowa kluczowego w deklaracji funkcji klasy pochodnej, kompilator nie będzie w stanie stwierdzić, czy zamierzasz zastąpić funkcję z klasy podstawowej.

Na tę pułapkę na szczęście jednak reaguje funkcja jawnego przesłonięcia języka C ++ 11 , która pozwala kodowi źródłowemu wyraźnie określić, że funkcja członka ma zastąpić funkcję klasy podstawowej:

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

Kompilator wygeneruje błąd czasu kompilacji, a błąd programowania stanie się natychmiast oczywisty (być może funkcja Pochodna powinna przyjąć floatargument jako argument).

Zobacz WP: C ++ 11 .

Colin D. Bennett
źródło
11

Dodanie słowa kluczowego „wirtualnego” jest dobrą praktyką, ponieważ poprawia czytelność, ale nie jest konieczne. Funkcje zadeklarowane jako wirtualne w klasie bazowej i mające tę samą sygnaturę w klasach pochodnych są domyślnie uważane za „wirtualne”.

Sujay Ghosh
źródło
7

Kompilator nie ma różnicy, gdy piszesz virtualw klasie pochodnej lub pomijasz ją.

Ale musisz spojrzeć na klasę podstawową, aby uzyskać te informacje. Dlatego polecam dodać virtualsłowo kluczowe również w klasie pochodnej, jeśli chcesz pokazać człowiekowi, że ta funkcja jest wirtualna.

harfiarka
źródło
2

Istnieje znaczna różnica, gdy masz szablony i zaczynasz brać klasy podstawowe jako parametry szablonu:

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

Część zabawy jest to, że można teraz definiować funkcje interfejsu i non-Interface później do definiowania klas. Jest to przydatne do współpracy interfejsów między bibliotekami (nie polegaj na tym jako standardowym procesie projektowania pojedynczej biblioteki). Nic nie kosztuje, aby pozwolić na to dla wszystkich twoich zajęć - możesz nawet typedefB do czegoś, jeśli chcesz.

Zauważ, że jeśli to zrobisz, możesz chcieć zadeklarować również konstruktory kopiowania / przenoszenia jako szablony: zezwolenie na konstruowanie z różnych interfejsów pozwala „rzutować” między różnymi B<>typami.

Wątpliwe jest, czy należy dodać obsługę const A&w t_hello(). Zwykle powodem tego przepisywania jest odejście od specjalizacji opartej na dziedziczeniu na specjalizacji opartej na szablonie, głównie ze względu na wydajność. Jeśli nadal będziesz obsługiwać stary interfejs, nie będziesz w stanie wykryć (lub powstrzymać) starego użycia.

lorro
źródło
1

virtualKluczowe powinny być dodane do funkcji klasy bazowej, aby je przeciążać. W twoim przykładzie struct Ajest to klasa podstawowa. virtualnic nie znaczy do używania tych funkcji w klasie pochodnej. Jednak jeśli chcesz, aby twoja klasa pochodna była również samą klasą bazową i chcesz, aby ta funkcja była nadpisywalna, wtedy musiałbyś ją virtualtam umieścić .

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

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

Tutaj Cdziedziczy po B, więc Bnie jest klasą bazową (jest to również klasa pochodna), a Cjest klasą pochodną. Schemat dziedziczenia wygląda następująco:

A
^
|
B
^
|
C

Dlatego należy umieścić virtualfunkcje przed potencjalnymi klasami podstawowymi, które mogą mieć dzieci. virtualpozwala twoim dzieciom zastąpić twoje funkcje. Nie ma nic złego w umieszczaniu virtualprzed funkcjami wewnątrz klas pochodnych, ale nie jest to wymagane. Jest to zalecane, ponieważ jeśli ktoś chciałby odziedziczyć po klasie pochodnej, nie byłby zadowolony, że zastąpienie metody nie działa zgodnie z oczekiwaniami.

Dlatego stawiaj virtualprzed funkcjami we wszystkich klasach związanych z dziedziczeniem, chyba że wiesz na pewno, że klasa nie będzie miała dzieci, które musiałyby zastąpić funkcje klasy podstawowej. To dobra praktyka.

Galaktyka
źródło
0

Z pewnością dołączę słowo kluczowe Virtual dla klasy potomnej, ponieważ

  • ja. Czytelność.
  • ii. Ta klasa potomna może być wyprowadzona niżej, nie chcesz, aby konstruktor innej pochodnej klasy wywoływał tę funkcję wirtualną.
użytkownik2264698
źródło
1
Myślę, że ma na myśli, że bez oznaczenia funkcji potomnej jako wirtualnej, programista, który wywodzi się z klasy potomnej, może nie zdawać sobie sprawy, że funkcja faktycznie jest wirtualna (ponieważ nigdy nie spojrzał na klasę podstawową) i może potencjalnie wywołać ją podczas budowy ( które mogą, ale nie muszą zrobić dobrze).
PfhorSlayer