Dlaczego funkcja overridden w klasie pochodnej ukrywa inne przeciążenia klasy bazowej?

Rozważmy 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: In function `int main()':
test.cpp:31: error: no matching function for call to `Derived::gogo(int)'
test.cpp:21: note: candidates are: virtual void Derived::gogo(int*) 
test.cpp:33:2: warning: no newline at end of file
>Exit code: 1

Tutaj funkcja klasy pochodnej jest przyćmieniem wszystkich funkcji o tej samej nazwie (nie sygnaturze) w klasie bazowej. Jakoś to zachowanie C++ nie wygląda dobrze. Nie polimorficzny.

Author: Rob Kennedy, 2009-10-27

4 answers

Sądząc po treści twojego pytania (użyłeś słowa "Ukryj"), już wiesz, co się tutaj dzieje. Zjawisko to nazywane jest "ukrywaniem nazw". Z jakiegoś powodu, za każdym razem, gdy ktoś zadaje pytanie o dlaczego ukrywanie nazw ma miejsce, ludzie, którzy odpowiadają albo mówią, że to się nazywa" ukrywanie nazw "i wyjaśniają jak to działa (co prawdopodobnie już wiesz), albo wyjaśniają jak to nadpisać (o co nigdy nie pytałeś), ale nikt nie dba o to, aby zająć się faktycznym "dlaczego ukrywanie nazw"?" pytanie.

Decyzja, uzasadnieniem ukrywania nazwy, tj. dlaczego została ona zaprojektowana w C++, polega na unikaniu pewnych nieprzewidzianych, 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ążeń w danej klasie. Zapewne wiesz, że w C++ rozdzielczość przeciążeń działa poprzez wybór najlepszej funkcji z zestawu kandydatów. Odbywa się to poprzez dopasowanie typy argumentów do typów parametrów. Reguły dopasowywania mogą być czasami skomplikowane i często prowadzić 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ść drastyczną zmianę wyników rozdzielczości przeciążenia.

Na przykład, powiedzmy, że klasa bazowa B ma funkcję member foo, która pobiera parametr typu void *, a wszystkie wywołania do foo(NULL) są rozwiązywane do B::foo(void *). Powiedzmy, że jest no name hiding i to {[5] } jest widoczne w wielu różnych klasach malejących z B. Jednak powiedzmy, że w pewnym [pośrednim, odległym] potomku {[8] } klasy B zdefiniowana jest funkcja foo(int). Teraz, bez ukrywania nazwy D ma zarówno foo(void *), jak i foo(int) widoczne i biorące udział w rozwiązywaniu przeciążeń. Do której funkcji zostaną wywołane wywołania foo(NULL), jeśli zostaną wykonane przez obiekt typu D? Rozwiążą się do D::foo(int), ponieważ int jest lepszym dopasowaniem do zera całkowego (tj. NULL) niż jakiekolwiek typ wskaźnika. Tak więc, w całej hierarchii wywołania foo(NULL) rozwiązują się do jednej funkcji, podczas gdy w D (i pod) nagle rozwiązują się do innej.

[21]} 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 byłby częściowo zaktualizowany, co prowadziłoby do krojenia.

To zachowanie zostało uznane za niepożądane, gdy język został zaprojektowany. Jako lepsze podejście, zdecydowano się podążać za specyfikacją "ukrywanie nazwy" , co oznacza, że każda klasa zaczyna się od "czystego arkusza" w odniesieniu do każdej nazwy metody, którą deklaruje. Aby nadpisać to zachowanie, wymagane jest jawne działanie od użytkownika: pierwotnie ponowne deklarowanie odziedziczonych metod (obecnie przestarzałych), teraz jawne użycie using-declaration.

Jak słusznie zauważyłeś w swoim oryginalnym poście (mam na myśli uwagę "nie polimorficzną"), to zachowanie może być postrzegane jako naruszenie relacji IS-a pomiędzy klasami. To jest prawda, ale najwyraźniej wtedy zdecydowano, że w końcu ukrywanie nazw okaże się mniejszym złem.

 364
Author: AnT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-12 04:33:12

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

W tym przypadku, gogo(int*) jest znaleziony (sam) w pochodnym zakresie klasy, A ponieważ nie ma standardowej konwersji z int do int*, wyszukiwanie nie powiedzie się.

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

using Base::gogo;

...would pozwól, aby reguły wyszukiwania nazw odnajdywały wszystkich kandydatów, a tym samym rozwiązywanie przeciążeń przebiegało zgodnie z oczekiwaniami.

 39
Author: Drew Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-27 04:29:21

To jest "z założenia". W C++ rozdzielczość przeciążeń dla tego typu metody działa jak poniżej.

  • zaczynając od typu odniesienia, 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" na tym typie znaleźć pasujące przeciążenie

Ponieważ Derived nie ma pasującej funkcji o nazwie "gogo", rozdzielczość przeciążenia nie działa.

 13
Author: JaredPar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-27 04:28:34

Ukrywanie nazw ma sens, ponieważ zapobiega niejednoznaczności w rozdzielczości nazw.

Rozważ ten kod:

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

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

Derived dobj;

Jeśli Base::func(float) nie była ukryta przez Derived::func(double) w pochodnej, wywołalibyśmy funkcję klasy bazowej podczas wywoływania dobj.func(0.f), nawet jeśli float może być promowany do dwójki.

Numer referencyjny: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

 2
Author: Sandeep Singh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-26 16:12:13