Warum verbirgt eine überschriebene Funktion in der abgeleiteten Klasse andere Überladungen der Basisklasse?

219

Betrachten Sie den Code:

#include <stdio.h>

class Base {
public: 
    virtual void gogo(int a){
        printf(" Base :: gogo (int) \n");
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*) \n");
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*) \n");
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

Habe diesen Fehler bekommen:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: In der Funktion `int main () ':
test.cpp: 31: Fehler: Keine passende Funktion für den Aufruf von `Derived :: gogo (int) '
test.cpp: 21: Hinweis: Kandidaten sind: virtual void Derived :: gogo (int *) 
test.cpp: 33: 2: Warnung: Kein Zeilenumbruch am Dateiende
> Exit-Code: 1

Hier verdunkelt die Funktion der abgeleiteten Klasse alle gleichnamigen Funktionen (keine Signatur) in der Basisklasse. Irgendwie sieht dieses Verhalten von C ++ nicht in Ordnung aus. Nicht polymorph.

Aman Aggarwal
quelle
8
Geniale Frage, das habe ich erst kürzlich entdeckt
Matt Joiner
11
Ich denke, Bjarne (von dem Link, den Mac gepostet hat) hat es am besten in einem Satz ausgedrückt: "In C ++ gibt es keine Überladung über Bereiche hinweg - abgeleitete Klassenbereiche sind keine Ausnahme von dieser allgemeinen Regel."
Sivabudh
7
@ Ashish Dieser Link ist defekt. Hier ist die richtige (ab sofort) - stroustrup.com/bs_faq2.html#overloadderived
nsane
3
Wollte auch darauf hinweisen, dass das obj.Base::gogo(7);immer noch funktioniert, indem man die versteckte Funktion aufruft.
Forumulator

Antworten:

406

Nach dem Wortlaut Ihrer Frage zu urteilen (Sie haben das Wort "verstecken" verwendet) wissen Sie bereits, was hier vor sich geht. Das Phänomen wird "Namensverstecken" genannt. Aus irgendeinem Grund sagen die Antwortenden jedes Mal, wenn jemand eine Frage stellt, warum das Verstecken von Namen erfolgt, dass dies als "Verstecken von Namen" bezeichnet wird, und erklären, wie es funktioniert (was Sie wahrscheinlich bereits wissen), oder erklären, wie Sie es überschreiben (was Sie tun) nie gefragt), aber niemand scheint sich darum zu kümmern, die eigentliche "Warum" -Frage anzusprechen.

Die Entscheidung, die Begründung für das Ausblenden des Namens, dh warum er tatsächlich in C ++ entwickelt wurde, besteht darin, bestimmte kontraintuitive, unvorhergesehene und potenziell gefährliche Verhaltensweisen zu vermeiden, die auftreten könnten, wenn der geerbte Satz überladener Funktionen mit dem aktuellen Satz von gemischt werden könnte Überladungen in der angegebenen Klasse. Sie wissen wahrscheinlich, dass die Überlastungsauflösung in C ++ funktioniert, indem Sie die beste Funktion aus der Gruppe der Kandidaten auswählen. Dies erfolgt durch Abgleichen der Argumenttypen mit den Parametertypen. Die Übereinstimmungsregeln können manchmal kompliziert sein und oft zu Ergebnissen führen, die von einem unvorbereiteten Benutzer als unlogisch empfunden werden. Das Hinzufügen neuer Funktionen zu einer Reihe zuvor vorhandener Funktionen kann zu einer drastischen Verschiebung der Ergebnisse der Überlastungsauflösung führen.

Angenommen, die Basisklasse Bverfügt über eine Elementfunktion foo, die einen Parameter vom Typ akzeptiert void *, und alle aufrufenden Aufrufe foo(NULL)werden aufgelöst B::foo(void *). Nehmen wir an, es gibt keinen Namen, der sich versteckt, und dies B::foo(void *)ist in vielen verschiedenen Klassen sichtbar, die von abstammen B. Nehmen wir jedoch an, in einem [indirekten, entfernten] Nachkommen Dder Klasse ist Beine Funktion foo(int)definiert. Nun, ohne Namen versteckt Dhat beide foo(void *)und foo(int)sichtbar und die Teilnahme an der Überladungsauflösung. Zu welcher Funktion werden die Aufrufe aufgelöst foo(NULL), wenn sie über ein Objekt vom Typ ausgeführt werden D? Sie werden sich auflösen D::foo(int), da dies inteine bessere Übereinstimmung für das Integral Null ist (dhNULL) als jeder Zeigertyp. Während der gesamten Hierarchie werden Aufrufe zur foo(NULL)Auflösung in eine Funktion ausgeführt, während sie in D(und unter) plötzlich in eine andere aufgelöst werden.

Ein weiteres Beispiel finden Sie in Das Design und die Entwicklung von C ++ , Seite 77:

class Base {
    int x;
public:
    virtual void copy(Base* p) { x = p-> x; }
};

class Derived{
    int xx;
public:
    virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copy Base part of b
    b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}

Ohne diese Regel würde der Status von b teilweise aktualisiert, was zu einer Aufteilung führen würde.

Dieses Verhalten wurde bei der Gestaltung der Sprache als unerwünscht angesehen. Als besserer Ansatz wurde beschlossen, die Spezifikation "Name verstecken" zu befolgen, was bedeutet, dass jede Klasse in Bezug auf jeden deklarierten Methodennamen mit einem "sauberen Blatt" beginnt. Um dieses Verhalten zu überschreiben, muss der Benutzer eine explizite Aktion ausführen: ursprünglich eine erneute Deklaration geerbter Methoden (derzeit veraltet), jetzt eine explizite Verwendung der using-Deklaration.

Wie Sie in Ihrem ursprünglichen Beitrag richtig festgestellt haben (ich beziehe mich auf die Bemerkung "Nicht polymorph"), kann dieses Verhalten als Verstoß gegen die IS-A-Beziehung zwischen den Klassen angesehen werden. Dies ist wahr, aber anscheinend wurde damals entschieden, dass sich das Verstecken von Namen am Ende als weniger böse erweisen würde.

Ameise
quelle
22
Ja, das ist eine echte Antwort auf die Frage. Danke dir. Ich war auch neugierig.
Omnifarious
4
Gute Antwort! Aus praktischen Gründen würde die Kompilierung wahrscheinlich viel langsamer werden, wenn die Namenssuche jedes Mal ganz nach oben gehen müsste.
Drew Hall
6
(Alte Antwort, ich weiß.) Nun nullptrwürde ich Ihrem Beispiel widersprechen, indem ich sage: "Wenn Sie die void*Version aufrufen möchten , sollten Sie einen Zeigertyp verwenden." Gibt es ein anderes Beispiel, wo dies schlecht sein kann?
GManNickG
3
Das Verstecken des Namens ist nicht wirklich böse. Die "is-a" -Beziehung ist noch vorhanden und über die Basisschnittstelle verfügbar. Vielleicht d->foo()bekommen Sie das "Is-a Base" nicht, aber es static_cast<Base*>(d)->foo() wird , einschließlich des dynamischen Versands.
Kerrek SB
12
Diese Antwort ist nicht hilfreich, da sich das angegebene Beispiel mit oder ohne Verstecken gleich verhält: D :: foo (int) wird entweder aufgerufen, weil es besser übereinstimmt oder weil B: foo (int) ausgeblendet ist.
Richard Wolf
46

Die Regeln für die Namensauflösung besagen, dass die Namenssuche im ersten Bereich stoppt, in dem ein übereinstimmender Name gefunden wird. Zu diesem Zeitpunkt werden die Regeln für die Überlastungsauflösung aktiviert, um die beste Übereinstimmung der verfügbaren Funktionen zu finden.

In diesem Fall gogo(int*)wird (allein) im Bereich der abgeleiteten Klasse gefunden, und da es keine Standardkonvertierung von int nach int * gibt, schlägt die Suche fehl.

Die Lösung besteht darin, die Basisdeklarationen über eine using-Deklaration in der Klasse Derived einzufügen:

using Base::gogo;

... würde es den Regeln für die Namenssuche ermöglichen, alle Kandidaten zu finden, und somit würde die Überlastungsauflösung wie erwartet verlaufen.

Drew Hall
quelle
10
OP: "Warum verbirgt eine überschriebene Funktion in der abgeleiteten Klasse andere Überladungen der Basisklasse?" Diese Antwort: "Weil es tut".
Richard Wolf
12

Dies ist "By Design". In C ++ funktioniert die Überlastungsauflösung für diese Art von Methode wie folgt.

  • Beginnen Sie mit dem Typ der Referenz und gehen Sie dann zum Basistyp. Suchen Sie den ersten Typ mit einer Methode namens "gogo".
  • Wenn nur Methoden mit dem Namen "gogo" für diesen Typ berücksichtigt werden, wird eine passende Überladung gefunden

Da Derived keine passende Funktion namens "gogo" hat, schlägt die Überlastungsauflösung fehl.

JaredPar
quelle
2

Das Ausblenden von Namen ist sinnvoll, da es Mehrdeutigkeiten bei der Namensauflösung verhindert.

Betrachten Sie diesen Code:

class Base
{
public:
    void func (float x) { ... }
}

class Derived: public Base
{
public:
    void func (double x) { ... }
}

Derived dobj;

Wenn dies in Derived Base::func(float)nicht verborgen Derived::func(double)wäre, würden wir beim Aufrufen die Basisklassenfunktion aufrufen dobj.func(0.f), obwohl ein Float zu einem Double heraufgestuft werden kann.

Referenz: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

Sandeep Singh
quelle