Rückgabetyp der virtuellen C ++ - Funktion

Antworten:

86

In einigen Fällen ist es für eine abgeleitete Klasse zulässig, eine virtuelle Funktion mit einem anderen Rückgabetyp zu überschreiben, solange der Rückgabetyp mit dem ursprünglichen Rückgabetyp kovariant ist . Betrachten Sie beispielsweise Folgendes:

class Base {
public:
    virtual ~Base() {}

    virtual Base* clone() const = 0;
};

class Derived: public Base {
public:
    virtual Derived* clone() const {
        return new Derived(*this);
    }
};

Hier Basedefiniert eine rein virtuelle Funktion aufgerufen , clonedass die Renditen ein Base *. In der abgeleiteten Implementierung wird diese virtuelle Funktion mit einem Rückgabetyp von überschrieben Derived *. Obwohl der Rückgabetyp nicht mit dem in der Basis identisch ist, ist dies absolut sicher, da Sie jederzeit schreiben würden

Base* ptr = /* ... */
Base* clone = ptr->clone();

Der Aufruf von clone()gibt immer einen Zeiger auf ein BaseObjekt zurück, da Derived*dieser Zeiger implizit in a konvertierbar ist Base*und die Operation genau definiert ist , selbst wenn er a zurückgibt .

Im Allgemeinen wird der Rückgabetyp einer Funktion niemals als Teil ihrer Signatur betrachtet. Sie können eine Mitgliedsfunktion mit jedem Rückgabetyp überschreiben, solange der Rückgabetyp kovariant ist.

templatetypedef
quelle
9
Diese "Sie können eine Mitgliedsfunktion mit jedem Rückgabetyp überschreiben" ist nicht korrekt. Sie können überschreiben, solange der Rückgabetyp identisch oder kovariant ist (was Sie erklärt haben). Es gibt hier keinen allgemeineren Fall.
Bronekk
1
@ bronekk- Der Rest des von Ihnen zitierten Satzes besagt dann, dass der neue Rückgabetyp überall dort verwendet werden muss, wo der ursprüngliche Typ wäre; Das heißt, der neue Typ ist mit dem Original kovariant.
Templatetypedef
dieser Rest des Satzes ist nicht korrekt; Stellen Sie sich vor, Sie ersetzen Base*durch longund Derived*mit int(oder umgekehrt, spielt keine Rolle). Es wird nicht funktionieren.
Bronekk
@ bronekk- Ah ja, daran habe ich nicht gedacht! Vielen Dank für den Hinweis.
Templatetypedef
1
Dass der Typ überall dort verwendet werden kann, wo der ursprüngliche Typ wäre, und Kovarianz sind zwei verschiedene Kontexte. Kovariant bedeutet, dass die Beziehung zwischen den Typen, die die Funktion implementieren, und die Beziehung zwischen den zurückgegebenen Typen auf dieselbe Weise variiert . Contravariant (obwohl in C ++ nicht nützlich) ist der entgegengesetzte Kontext. Einige Sprachen erlauben kontravariante Argumente beim dynamischen Versand (wenn die Basis ein Objekt vom Typ T nimmt, kann der abgeleitete Typ ein T 'annehmen, wobei T' eine Basis von T ist - wenn Sie eine Hierarchie nach unten gehen, bewegen Sie die andere nach oben ).
David Rodríguez - Dribeas
53

Ja. Die Rückgabetypen dürfen unterschiedlich sein, solange sie kovariant sind . Der C ++ - Standard beschreibt es so (§10.3 / 5):

Der Rückgabetyp einer überschreibenden Funktion muss entweder mit dem Rückgabetyp der überschriebenen Funktion identisch oder mit den Klassen der Funktionen kovariant sein . Wenn eine Funktion eine Funktion D::füberschreibt B::f, ist der Rückgabetyp der Funktionen kovariant, wenn sie die folgenden Kriterien erfüllen:

  • beide sind Zeiger auf Klassen oder Verweise auf Klassen 98)
  • Die Klasse im Rückgabetyp von B::fist dieselbe Klasse wie die Klasse im Rückgabetyp von D::foder ist eine eindeutige direkte oder indirekte Basisklasse der Klasse im Rückgabetyp von D::fund ist in zugänglichD
  • Beide Zeiger oder Referenzen haben dieselbe Lebenslaufqualifikation, und der Klassentyp im Rückgabetyp von D::fhat dieselbe Lebenslaufqualifikation wie oder weniger Lebenslaufqualifikation als der Klassentyp im Rückgabetyp von B::f.

In Fußnote 98 wird darauf hingewiesen, dass "mehrstufige Zeiger auf Klassen oder Verweise auf mehrstufige Zeiger auf Klassen nicht zulässig sind."

Kurz gesagt, wenn Des sich um einen Subtyp von handelt B, muss der Rückgabetyp der Funktion in Dein Subtyp des Rückgabetyps der Funktion in sein B. Das häufigste Beispiel ist, wenn die Rückgabetypen selbst auf Dund basieren B, dies aber nicht sein müssen. Betrachten Sie dies, wo wir zwei getrennte Typhierarchien haben:

struct Base { /* ... */ };
struct Derived: public Base { /* ... */ };

struct B {
  virtual Base* func() { return new Base; }
  virtual ~B() { }
};
struct D: public B {
  Derived* func() { return new Derived; }
};

int main() {
  B* b = new D;
  Base* base = b->func();
  delete base;
  delete b;
}

Der Grund dafür ist, dass jeder Aufrufer von funceinen BaseZeiger erwartet . Jeder BaseZeiger reicht aus. Wenn also D::funcversprochen wird, immer einen DerivedZeiger zurückzugeben, erfüllt er immer den von der Vorfahrenklasse festgelegten Vertrag, da jeder DerivedZeiger implizit in einen BaseZeiger konvertiert werden kann . So erhalten Anrufer immer das, was sie erwarten.


In einigen Sprachen können nicht nur die Rückgabetypen variieren, sondern auch die Parametertypen der überschreibenden Funktion. Wenn sie das tun, müssen sie normalerweise kontravariant sein . Das heißt, wenn a B::fakzeptiert wird Derived*, D::fdarf a akzeptiert werden Base*. Nachkommen dürfen lockerer sein, was sie akzeptieren, und strenger in dem, was sie zurückgeben. In C ++ ist keine Kontravarianz vom Parametertyp zulässig. Wenn Sie die Parametertypen ändern, betrachtet C ++ diese als brandneue Funktion, sodass Sie anfangen, zu überladen und sich zu verstecken. Weitere Informationen zu diesem Thema finden Sie unter Kovarianz und Kontravarianz (Informatik) in Wikipedia.

Rob Kennedy
quelle
2
Ist dies eine tatsächliche Funktion oder ein Nebeneffekt des Rückgabetyps, der bei der Auflösung nicht verwendet wird?
Martin York
1
@ Martin, definitiv ein Feature. Ich bin mir ziemlich sicher, dass die Überlastungsauflösung nichts damit zu tun hat. Der Rückgabetyp wird verwendet, wenn Sie eine Funktion überschreiben.
Rob Kennedy