Dlaczego funkcja przesłonięta w klasie pochodnej ukrywa inne przeciążenia klasy podstawowej?

220

Rozważ kod:

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

Mam ten błąd:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: W funkcji `int main () ':
test.cpp: 31: błąd: brak pasującej funkcji dla wywołania `Derived :: gogo (int) '
test.cpp: 21: uwaga: kandydatami są: virtual void Derived :: gogo (int *) 
test.cpp: 33: 2: ostrzeżenie: brak nowej linii na końcu pliku
> Kod wyjścia: 1

Tutaj funkcja klasy pochodnej przyćmiewa wszystkie funkcje o tej samej nazwie (nie sygnaturze) w klasie bazowej. Jakoś to zachowanie C ++ nie wygląda OK. Nie polimorficzny.

Aman Aggarwal
źródło
8
genialne pytanie, odkryłem to dopiero niedawno
Matt Joiner
11
Myślę, że Bjarne (z linku opublikowanego przez Maca) najlepiej ujął to w jednym zdaniu: „W C ++ nie występuje przeciążenie między zakresami - zakresy klas pochodnych nie są wyjątkiem od tej ogólnej zasady”.
sivabudh
7
@Ashish Ten link jest uszkodzony. Oto poprawny (na razie) - stroustrup.com/bs_faq2.html#overloadderived
nsane
3
Chciałem również zauważyć, że obj.Base::gogo(7);nadal działa, wywołując funkcję ukrytą.
forumulator

Odpowiedzi:

406

Sądząc po sformułowaniu pytania (użyłeś słowa „ukryj”), już wiesz, co się tutaj dzieje. Zjawisko to nazywa się „ukrywaniem nazwy”. Z jakiegoś powodu za każdym razem, gdy ktoś zadaje pytanie o to, dlaczego dzieje się ukrywanie imienia, ludzie, którzy odpowiadają, albo mówią, że to się nazywa „ukrywanie imienia” i wyjaśniają, jak to działa (co prawdopodobnie już wiesz), lub wyjaśniają, jak to zmienić (co ty nigdy o to nie pytano), ale wydaje się, że nikomu nie zależy na odpowiedzi na pytanie „dlaczego”.

Decyzja, uzasadnienie ukrywania nazwy, tj. Dlaczego tak naprawdę została zaprojektowana w C ++, polega na uniknięciu pewnych sprzecznych z intuicją, nieprzewidzianych i potencjalnie niebezpiecznych zachowań, które mogłyby mieć miejsce, gdyby odziedziczony zestaw przeciążonych funkcji mógł mieszać się z bieżącym zestawem przeciążenia w danej klasie. Prawdopodobnie wiesz, że w C ++ rozwiązywanie przeciążeń działa poprzez wybranie najlepszej funkcji z zestawu kandydatów. Odbywa się to poprzez dopasowanie typów argumentów do typów parametrów. Reguły dopasowania mogą być czasem skomplikowane i często prowadzą do wyników, które mogą być postrzegane jako nielogiczne przez nieprzygotowanego użytkownika. Dodanie nowych funkcji do zestawu wcześniej istniejących może spowodować dość drastyczne przesunięcie wyników rozwiązywania przeciążenia.

Załóżmy na przykład, że klasa podstawowa Bma funkcję fooskładową void *, która przyjmuje parametr typu , a wszystkie wywołania foo(NULL)są rozpoznawane B::foo(void *). Powiedzmy, że nie kryje się żadna nazwa i B::foo(void *)jest to widoczne w wielu różnych klasach zstępujących B. Powiedzmy jednak, że u jakiegoś [pośredniego, zdalnego] potomka Dklasy zdefiniowana jest Bfunkcja foo(int). Teraz bez nazwy ukryciu Dma zarówno foo(void *)i foo(int)widoczne i uczestnicząc w rozdzielczości przeciążenia. Jaką funkcję będą foo(NULL)rozstrzygać wywołania , jeśli zostaną wykonane za pomocą obiektu typu D? Rozwiążą się D::foo(int), ponieważ intjest to lepsze dopasowanie dla całki zerowej (tjNULL) niż jakikolwiek typ wskaźnika. Tak więc w całej hierarchii wezwania do foo(NULL)rozwiązania jednej funkcji, podczas gdy w D(i poniżej) nagle przechodzą do innej funkcji.

Inny przykład podano w The Design and Evolution of C ++ , strona 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*)
}

Bez tej reguły stan b zostałby częściowo zaktualizowany, co doprowadziłoby do krojenia.

To zachowanie zostało uznane za niepożądane, gdy język został zaprojektowany. Jako lepsze podejście, postanowiono zastosować się do specyfikacji „ukrywania nazw”, co oznacza, że ​​każda klasa zaczyna się od „czystego arkusza” w odniesieniu do każdej deklarowanej nazwy metody. Aby zastąpić to zachowanie, wymagane jest wyraźne działanie od użytkownika: pierwotnie ponowne zadeklarowanie odziedziczonych metod (obecnie nieaktualne), teraz jawne użycie deklaracji użycia.

Jak prawidłowo zauważyłeś w swoim oryginalnym poście (odnoszę się do uwagi „Nie polimorficzny”), takie zachowanie może być postrzegane jako naruszenie relacji IS-A między klasami. To prawda, ale najwyraźniej wtedy zdecydowano, że ukrywanie nazwy okaże się mniejszym złem.

Mrówka
źródło
22
Tak, to jest prawdziwa odpowiedź na pytanie. Dziękuję Ci. Też byłem ciekawy.
Wszechobecny
4
Świetna odpowiedź! Ponadto ze względów praktycznych kompilacja byłaby prawdopodobnie znacznie wolniejsza, gdyby wyszukiwanie nazw musiało za każdym razem sięgać szczytu.
Drew Hall
6
(Stara odpowiedź, wiem.) Sprzeciwiłbym nullptrsię twojemu przykładowi, mówiąc „jeśli chcesz wywołać void*wersję, powinieneś użyć typu wskaźnika”. Czy jest inny przykład, w którym może to być złe?
GManNickG
3
Ukrywanie imienia nie jest tak naprawdę złe. Relacja „jest-a” jest nadal dostępna i dostępna za pośrednictwem interfejsu podstawowego. Więc może d->foo()nie dostaniesz „Is-a Base”, ale static_cast<Base*>(d)->foo() będzie , włączając dynamiczną wysyłkę.
Kerrek SB
12
Ta odpowiedź jest nieprzydatna, ponieważ podany przykład zachowuje się tak samo z ukryciem lub bez: D :: foo (int) zostanie wywołany albo dlatego, że lepiej pasuje, albo dlatego, że ukrył B: foo (int).
Richard Wolf
46

Reguły rozpoznawania nazw mówią, że wyszukiwanie nazw kończy się w pierwszym zakresie, w którym znaleziono pasującą nazwę. W tym momencie uruchamiane są reguły rozwiązywania problemu przeciążenia, aby znaleźć najlepsze dopasowanie dostępnych funkcji.

W tym przypadku gogo(int*)znajduje się (sam) w zakresie klasy Derived, a ponieważ nie ma standardowej konwersji z int na int *, wyszukiwanie kończy się niepowodzeniem.

Rozwiązaniem jest wprowadzenie deklaracji bazowych za pomocą deklaracji using w klasie Derived:

using Base::gogo;

... pozwoliłby regułom wyszukiwania nazw znaleźć wszystkich kandydatów, a zatem rozwiązanie problemu przeciążenia przebiegałoby zgodnie z oczekiwaniami.

Drew Hall
źródło
10
OP: „Dlaczego przesłonięta funkcja w klasie pochodnej ukrywa inne przeciążenia klasy podstawowej?” Ta odpowiedź: „Ponieważ tak jest”.
Richard Wolf
12

To jest „Z założenia”. W C ++ rozdzielczość przeciążenia dla tego typu metody działa jak poniżej.

  • Zaczynając od typu odwołania, a następnie przechodząc do typu podstawowego, znajdź pierwszy typ, który ma metodę o nazwie „gogo”
  • Biorąc pod uwagę tylko metody o nazwie „gogo” tego typu, znajdź pasujące przeciążenie

Ponieważ Derived nie ma pasującej funkcji o nazwie „gogo”, rozwiązywanie problemów z przeciążeniem kończy się niepowodzeniem.

JaredPar
źródło
2

Ukrywanie nazw ma sens, ponieważ zapobiega dwuznacznościom w rozpoznawaniu nazw.

Rozważ ten kod:

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

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

Derived dobj;

Gdyby Base::func(float)nie było to ukryte Derived::func(double)w Derived, wywołalibyśmy funkcję klasy bazowej podczas wywoływania dobj.func(0.f), nawet jeśli liczba zmiennoprzecinkowa może zostać podwojona.

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

Sandeep Singh
źródło