Zwracany typ funkcji wirtualnej C ++

Odpowiedzi:

86

W niektórych przypadkach tak, klasa pochodna może przesłonić funkcję wirtualną przy użyciu innego typu zwracanego, o ile typ zwracany jest kowariantny z pierwotnym typem zwracanym. Na przykład rozważ następujące kwestie:

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

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

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

Tutaj Basedefiniuje czystą funkcję wirtualną o nazwie, clonektóra zwraca plik Base *. W implementacji pochodnej ta funkcja wirtualna jest zastępowana przy użyciu zwracanego typu Derived *. Chociaż typ zwrotu nie jest taki sam jak w bazie, jest to całkowicie bezpieczne, ponieważ w każdej chwili możesz napisać

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

Wywołanie clone()to zawsze zwróci wskaźnik do Baseobiektu, ponieważ nawet jeśli zwraca a Derived*, ten wskaźnik jest niejawnie konwertowany na a, Base*a operacja jest dobrze zdefiniowana.

Mówiąc bardziej ogólnie, typ zwracany funkcji nigdy nie jest uważany za część jej podpisu. Funkcję składową można zastąpić dowolnym typem zwracanym, o ile typ zwracany jest kowariantny.

templatetypedef
źródło
9
To „Możesz zastąpić funkcję składową dowolnym zwracanym typem” nie jest poprawne. Możesz przesłonić, o ile typ zwracany jest identyczny lub kowariantny (co wyjaśniłeś), kropka. Nie ma tutaj bardziej ogólnego przypadku.
bronekk
1
@ bronekk - Reszta cytowanego zdania stwierdza, że ​​nowy typ zwracany musi być użyteczny wszędzie tam, gdzie byłby typ oryginalny; to znaczy, nowy typ jest kowariantny z oryginałem.
templatetypedef
że reszta zdania jest nieprawidłowa; wyobrazić zastąpienie Base*z longi Derived*z int(lub na odwrót, nie ma znaczenia). To nie zadziała.
bronekk
@ bronekk- Ach tak, nie pomyślałem o tym! Dziękuję za zwrócenie uwagi.
templatetypedef
1
Typ jest użyteczny wszędzie tam, gdzie byłby typ oryginalny, a kowariancja to dwa różne konteksty. Kowariantne oznacza, że ​​relacja między typami, które implementują funkcję, a relacja między zwracanymi typami różni się w ten sam sposób. Kontrawariant (choć nie jest przydatny w C ++) to kontekst odwrotny. Niektóre języki dopuszczają kontrawariantne argumenty w dynamicznej wysyłce (jeśli baza przyjmuje obiekt typu T, typ pochodny może przyjąć T 'gdzie T' jest podstawą T - schodząc w dół o jedną hierarchię przesuwasz się w górę o drugą ).
David Rodríguez - drybling
54

Tak. Typy zwracane mogą być różne, o ile są kowariantne . Standard C ++ opisuje to w następujący sposób (§ 10.3 / 5):

Zwracany typ funkcji przesłaniającej powinien być identyczny z zwracanym typem funkcji przesłoniętej, albo kowariantny z klasami funkcji. Jeśli funkcja D::fprzesłania funkcję B::f, zwracane typy funkcji są kowariantne, jeśli spełniają następujące kryteria:

  • oba są wskaźnikami do klas lub odniesieniami do klas 98)
  • klasa w zwracanym typie B::fjest tą samą klasą co klasa w zwracanym typie D::for, jest jednoznaczną bezpośrednią lub pośrednią klasą bazową klasy w zwracanym typie D::fi jest dostępna wD
  • oba wskaźniki lub odwołania mają tę samą kwalifikację CV, a typ klasy w zwracanym typie D::fma taką samą kwalifikację CV jak kwalifikację CV lub mniej niż typ klasy w zwracanym typie B::f.

Przypis 98 wskazuje, że „wielopoziomowe wskaźniki do klas lub odniesienia do wielopoziomowych wskaźników do klas są niedozwolone”.

W skrócie, jeśli Djest podtypem funkcji B, to zwracany typ funkcji w Dmusi być podtypem zwracanego typu funkcji w B. Najczęstszym przykładem jest sytuacja, w której typy zwracane są oparte na Di B, ale nie muszą. Rozważ to, gdy mamy dwie oddzielne hierarchie typów:

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

Powodem, dla którego to działa, jest to, że każdy wywołujący funcoczekuje Basewskaźnika. Dowolny Basewskaźnik wystarczy. Tak więc, jeśli D::funcobiecuje, że zawsze zwraca Derivedwskaźnik, to zawsze będzie spełniał kontrakt określony przez klasę nadrzędną, ponieważ każdy Derivedwskaźnik można niejawnie przekonwertować na Basewskaźnik. W ten sposób dzwoniący zawsze otrzymają to, czego oczekują.


Oprócz zezwalania na zmianę typu zwracanego, niektóre języki pozwalają również na zmianę typów parametrów funkcji przesłaniającej. Kiedy to robią, zwykle muszą być sprzeczne . Oznacza to, że jeśli B::fakceptuje a Derived*, to D::fbędzie mógł zaakceptować a Base*. Potomkowie mogą być bardziej rozluźnieni w tym, co przyjmą, i surowsi w tym, co zwrócą. C ++ nie zezwala na kontrawariancję typu parametru. Jeśli zmienisz typy parametrów, C ++ uzna to za zupełnie nową funkcję, więc zaczniesz się przeciążać i ukrywać. Więcej informacji na ten temat można znaleźć w artykule Kowariancja i kontrawariancja (informatyka) w Wikipedii.

Rob Kennedy
źródło
2
Czy jest to rzeczywista funkcja czy efekt uboczny zwracanego typu, który nie jest używany w rozdzielczości?
Martin York,
1
@Martin, zdecydowanie funkcja. Jestem prawie pewien, że rozdzielczość przeciążenia nie miała z tym nic wspólnego. Typ zwracany jest używany, jeśli przesłaniasz funkcję.
Rob Kennedy,