C ++ "virtuelles" Schlüsselwort für Funktionen in abgeleiteten Klassen. Ist es nötig?

221

Mit der unten angegebenen Strukturdefinition ...

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

Ansatz 1:

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

Ansatz 2:

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

Gibt es einen Unterschied zwischen diesen beiden Möglichkeiten, die Hallo-Funktion zu überschreiben?

Anarki
quelle
65
In C ++ 11 können Sie "void hello () override {}" schreiben, um explizit zu deklarieren, dass Sie eine virtuelle Methode überschreiben. Der Compiler schlägt fehl, wenn keine virtuelle Basismethode vorhanden ist und dieselbe Lesbarkeit aufweist wie das Platzieren von "virtuell" in der untergeordneten Klasse.
ShadowChaser
In C ++ 11 von gcc ist das Schreiben von void hello () override {} in der abgeleiteten Klasse in Ordnung, da die Basisklasse angegeben hat, dass die Methode hello () virtuell ist. Mit anderen Worten, die Verwendung des Wortes virtual in der abgeleiteten Klasse ist für gcc / g ++ ohnehin nicht erforderlich / obligatorisch. (Ich verwende gcc Version 4.9.2 auf einem RPi 3) Es empfiehlt sich jedoch, das Schlüsselwort virtual trotzdem in die Methode der abgeleiteten Klasse aufzunehmen.
Will

Antworten:

183

Sie sind genau das gleiche. Es gibt keinen Unterschied zwischen ihnen, außer dass der erste Ansatz mehr Eingabe erfordert und möglicherweise klarer ist.

James McNellis
quelle
25
Dies ist wahr, aber im Mozilla C ++ - Portabilitätshandbuch wird empfohlen, immer virtuell zu verwenden, da "einige Compiler" Warnungen ausgeben, wenn Sie dies nicht tun. Schade, dass sie keine Beispiele für solche Compiler erwähnen.
Sergei Tachenov
5
Ich möchte auch hinzufügen, dass das explizite Markieren als virtuell Sie daran erinnert, den Destruktor auch virtuell zu machen.
Lfalin
1
Nur zu erwähnen, dasselbe gilt für den virtuellen Destruktor
Atul
6
@SergeyTachenov laut Cliffords Kommentar zu seiner eigenen Antwort ist armcc ein Beispiel für solche Compiler.
Ruslan
4
@Rasmi, der neue Portabilitätsleitfaden ist hier , aber jetzt wird empfohlen, das overrideSchlüsselwort zu verwenden.
Sergei Tachenov
83

Die 'Virtualität' einer Funktion wird implizit weitergegeben. Mindestens ein von mir verwendeter Compiler generiert jedoch eine Warnung, wenn das virtualSchlüsselwort nicht explizit verwendet wird. Sie können es daher verwenden, um den Compiler ruhig zu halten.

Aus rein stilistischer Sicht macht das Einbeziehen des virtualSchlüsselworts dem Benutzer deutlich, dass die Funktion virtuell ist. Dies ist für jeden wichtig, der B weiter unterordnet, ohne die Definition von A überprüfen zu müssen. Für tiefe Klassenhierarchien wird dies besonders wichtig.

Clifford
quelle
12
Welcher Compiler ist das?
James McNellis
35
@ James: Armcc (ARMs Compiler für ARM-Geräte)
Clifford
55

Das virtualSchlüsselwort ist in der abgeleiteten Klasse nicht erforderlich. Hier ist die unterstützende Dokumentation aus dem C ++ Draft Standard (N3337) (Schwerpunkt Mine):

10.3 Virtuelle Funktionen

2 Wenn eine virtuelle Mitgliedsfunktion vfin einer Klasse Baseund in einer Klasse deklariert ist Derived, die direkt oder indirekt von Baseeiner Mitgliedsfunktion vfmit demselben Namen, Parametertypliste (8.3.5), Lebenslaufqualifikation und Referenzqualifizierer ( oder das Fehlen derselben), wie Base::vfdeklariert ist, ist dann Derived::vfauch virtuell ( unabhängig davon, ob es so deklariert ist oder nicht ) und überschreibt es Base::vf.

R Sahu
quelle
5
Dies ist bei weitem die beste Antwort hier.
Fantastischer Herr Fox
33

Nein, das virtualSchlüsselwort für die virtuellen Funktionsüberschreibungen abgeleiteter Klassen ist nicht erforderlich. Erwähnenswert ist jedoch eine damit verbundene Gefahr: ein Fehler beim Überschreiben einer virtuellen Funktion.

Der Fehler beim Überschreiben tritt auf, wenn Sie beabsichtigen, eine virtuelle Funktion in einer abgeleiteten Klasse zu überschreiben, aber einen Fehler in der Signatur machen, sodass eine neue und andere virtuelle Funktion deklariert wird. Diese Funktion kann eine Überladung der Basisklassenfunktion sein oder sich im Namen unterscheiden. virtualUnabhängig davon, ob Sie das Schlüsselwort in der abgeleiteten Klassenfunktionsdeklaration verwenden oder nicht, kann der Compiler nicht erkennen, dass Sie beabsichtigen, eine Funktion aus einer Basisklasse zu überschreiben.

Diese Gefahr wird jedoch dankenswerterweise durch die explizite C ++ 11-Funktion zum Überschreiben von Sprachen behoben, mit der der Quellcode klar angeben kann, dass eine Mitgliedsfunktion eine Basisklassenfunktion überschreiben soll:

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

Der Compiler gibt einen Fehler zur Kompilierungszeit aus und der Programmierfehler ist sofort offensichtlich (möglicherweise sollte die Funktion in Derived a floatals Argument verwendet haben).

Siehe WP: C ++ 11 .

Colin D Bennett
quelle
11

Das Hinzufügen des Schlüsselworts "virtuell" ist eine gute Vorgehensweise, da es die Lesbarkeit verbessert, jedoch nicht erforderlich ist. Funktionen, die in der Basisklasse als virtuell deklariert wurden und in den abgeleiteten Klassen dieselbe Signatur haben, werden standardmäßig als "virtuell" betrachtet.

Sujay Ghosh
quelle
7

Es gibt keinen Unterschied für den Compiler, wenn Sie das virtualin die abgeleitete Klasse schreiben oder weglassen.

Sie müssen sich jedoch die Basisklasse ansehen, um diese Informationen zu erhalten. Daher würde ich empfehlen, das virtualSchlüsselwort auch in die abgeleitete Klasse einzufügen , wenn Sie dem Menschen zeigen möchten, dass diese Funktion virtuell ist.

Harper
quelle
2

Es gibt einen erheblichen Unterschied, wenn Sie Vorlagen haben und Basisklassen als Vorlagenparameter verwenden:

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

Der lustige Teil davon ist, dass Sie jetzt Schnittstellen- und Nicht-Schnittstellenfunktionen definieren können, um später Klassen zu definieren. Dies ist nützlich für das Interworking von Schnittstellen zwischen Bibliotheken (verlassen Sie sich nicht darauf als Standardentwurfsprozess einer einzelnen Bibliothek). Es kostet Sie nichts, dies für alle Ihre Klassen zuzulassen - Sie könnten sogar typedefB zu etwas, wenn Sie möchten.

Beachten Sie, dass Sie in diesem Fall möglicherweise auch Kopier- / Verschiebungskonstruktoren als Vorlagen deklarieren möchten: Wenn Sie über verschiedene Schnittstellen konstruieren können, können Sie zwischen verschiedenen B<>Typen wechseln .

Es ist fraglich, ob Sie Unterstützung für const A&in hinzufügen sollten t_hello(). Der übliche Grund für dieses Umschreiben besteht darin, von einer vererbungsbasierten Spezialisierung zu einer vorlagenbasierten zu wechseln, hauptsächlich aus Leistungsgründen. Wenn Sie die alte Benutzeroberfläche weiterhin unterstützen, können Sie die alte Nutzung kaum erkennen (oder davon abhalten).

lorro
quelle
1

Das virtualSchlüsselwort sollte zu Funktionen einer Basisklasse hinzugefügt werden, um sie überschreibbar zu machen. In Ihrem Beispiel struct Aist die Basisklasse. virtualbedeutet nichts für die Verwendung dieser Funktionen in einer abgeleiteten Klasse. Wenn Sie jedoch möchten, dass Ihre abgeleitete Klasse auch selbst eine Basisklasse ist und diese Funktion überschreibbar ist, müssen Sie die virtualdort ablegen .

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

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

Hier Cerbt von B, Bist also nicht die Basisklasse (es ist auch eine abgeleitete Klasse), und Cist die abgeleitete Klasse. Das Vererbungsdiagramm sieht folgendermaßen aus:

A
^
|
B
^
|
C

Daher sollten Sie die virtualFunktionen vor potenzielle Basisklassen stellen, die möglicherweise Kinder haben. virtualermöglicht Ihren Kindern, Ihre Funktionen zu überschreiben. Es ist nichts Falsches daran, die virtualFunktionen innerhalb der abgeleiteten Klassen vor zu stellen, aber es ist nicht erforderlich. Es wird jedoch empfohlen, da jemand, der von Ihrer abgeleiteten Klasse erben möchte, nicht erfreut wäre, dass das Überschreiben der Methode nicht wie erwartet funktioniert.

Stellen Sie also virtualFunktionen in allen an der Vererbung beteiligten Klassen vor, es sei denn, Sie wissen sicher, dass die Klasse keine untergeordneten Elemente hat, die die Funktionen der Basisklasse überschreiben müssten. Es ist eine gute Praxis.

Galaxis
quelle
0

Ich werde auf jeden Fall das virtuelle Schlüsselwort für die untergeordnete Klasse einschließen, weil

  • ich. Lesbarkeit.
  • ii. Diese untergeordnete Klasse kann weiter unten abgeleitet werden. Sie möchten nicht, dass der Konstruktor der weiter abgeleiteten Klasse diese virtuelle Funktion aufruft.
user2264698
quelle
1
Ich denke, er meint, ohne dass die untergeordnete Funktion als virtuell markiert wird, kann ein Programmierer, der später aus der untergeordneten Klasse stammt, möglicherweise nicht erkennen, dass die Funktion tatsächlich virtuell ist (weil er die Basisklasse nie angesehen hat) und sie möglicherweise während der Erstellung aufrufen ( was das Richtige tun kann oder nicht).
PfhorSlayer