Чому перекрита функція у похідному класі приховує інші перевантаження базового класу?


220

Розглянемо код:

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

Отримав цю помилку:

> g ++ -педагогічний -Os test.cpp -випробування
test.cpp: У функції `int main () ':
test.cpp: 31: помилка: немає функції узгодження для виклику до `Виведеного :: gogo (int) '
test.cpp: 21: Примітка: кандидатами є: віртуальна порожнеча Отримано :: gogo (int *) 
test.cpp: 33: 2: попередження: у кінці файлу немає нового рядка
> Код виходу: 1

Тут функція класу Похідне затьмарює всі однойменні функції (не підписи) в базовому класі. Якось така поведінка C ++ не виглядає нормально. Не поліморфний.



8
геніальне запитання, я відкрив це лише нещодавно
Метт Столяр

11
Я думаю, що Bjarne (за посиланням на Mac розміщено) найкраще вказує в одному реченні: "У C ++ немає перевантажень по областям - отримані області застосування класів не є винятком із цього загального правила".
sivabudh

7
@Ashish Це посилання розірвано. Ось правильний (станом на сьогодні) - stroustrup.com/bs_faq2.html#overloadderived
nsane

3
Також хотів зазначити, що obj.Base::gogo(7);все ще працює, викликаючи приховану функцію.
форумутор

Відповіді:


406

Судячи з формулювання вашого запитання (ви вживали слово "сховатися"), ви вже знаєте, що тут відбувається. Явище називається «приховування імені». Чомусь кожен раз, коли хтось задає питання про те, чому трапляється приховування імені, люди, які відповідають або говорять, що це називається "приховування імені", і пояснюють, як воно працює (що ви, мабуть, вже знаєте), або пояснюють, як його переосмислити (що ви ніколи про це не питали), але, здається, ніхто не піклується про вирішення актуального питання "чому".

Рішення, обґрунтування приховування імені, тобто чому воно насправді було розроблено на C ++, полягає в тому, щоб уникнути певних контрінтуїтивних, непередбачуваних та потенційно небезпечних поведінок, які можуть мати місце, якщо спадкоємному набору перевантажених функцій було дозволено змішуватися з поточним набором перевантаження в даному класі. Ви, мабуть, знаєте, що в C ++ роздільна здатність перевантаження працює, вибираючи найкращу функцію з набору кандидатів. Це робиться шляхом підбору типів аргументів до типів параметрів. Правила відповідності часом можуть бути складними і часто призводять до результатів, які непідготовлений користувач може сприймати як нелогічний. Додавання нових функцій до набору вже існуючих може призвести до досить різкого зрушення результатів роздільної здатності перевантаження.

Наприклад, скажімо, базовий клас Bмає функцію-член, fooякий приймає параметр типу void *, і всі виклики, для foo(NULL)яких вирішено B::foo(void *). Скажімо, немає імені, яке ховається, і це B::foo(void *)видно в багатьох різних класах, що походять від B. Однак, скажімо, в якомусь [непрямому, віддаленому] нащадку Dкласу Bвизначена функція foo(int). Тепер без приховування імен Dє foo(void *)і foo(int)видиме, і участь у вирішенні перевантажень. До якої функції будуть foo(NULL)вирішуватися дзвінки , якщо вони здійснюються через тип об'єкта D? Вони вирішуватимуться D::foo(int), оскільки intце краща відповідність інтегральному нулю (тобтоNULL), ніж будь-який тип вказівника. Отже, протягом всієї ієрархії заклики foo(NULL)вирішувати одну функцію, тоді як в D(і під) вони раптово вирішуються на іншу.

Інший приклад наведено в «Дизайн та еволюція C ++» , стор. 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*)
}

Без цього правила стан b було б частково оновлене, що призвело б до скорочення.

Така поведінка вважалася небажаною, коли мова була розроблена. В якості кращого підходу було вирішено дотримуватися специфікації "приховування імені", тобто кожен клас починається з "чистого аркуша" стосовно кожного названого методу. Для того, щоб змінити таку поведінку, від користувача потрібна чітка дія: спочатку передекларація успадкованих методів (методів) (наразі застаріла), тепер явне використання використання-декларації.

Як ви правильно помітили у своєму початковому дописі (я маю на увазі зауваження "Не поліморфне"), ця поведінка може розглядатися як порушення відносин IS-A між класами. Це правда, але, мабуть, тоді було вирішено, що врешті-решт приховування імені виявиться меншим злом.


22
Так, це справжня відповідь на питання. Дякую. Мені теж було цікаво.
всезнайко

4
Чудова відповідь! Крім того, як практична справа, компіляція, ймовірно, буде набагато повільнішою, якби пошук імен повинен був щоразу йти до вершини.
Дрю Холл

6
(Стару відповідь, я знаю.) Тепер nullptrя заперечу проти вашого прикладу, кажучи "якщо ви хочете зателефонувати у void*версію, вам слід використовувати тип вказівника". Чи є інший приклад, коли це може бути погано?
GManNickG

3
Ім'я, що ховається, насправді не є злою. Відносини "є-а" все ще існують і доступні через базовий інтерфейс. Тож, можливо d->foo(), не ви отримаєте "Is-a Base", але static_cast<Base*>(d)->foo() буде , включаючи динамічну відправку.
Керрек СБ

12
Ця відповідь не є корисною, оскільки наведений приклад поводиться так само з приховуванням або без нього: D :: foo (int) буде називатися або тому, що це кращий збіг, або тому, що він приховав B: foo (int).
Річард Вольф

46

Правила вирішення імен говорять, що пошук імен зупиняється на першій області, в якій знайдено відповідне ім'я. У цей момент починаються правила вирішення перевантаження, щоб знайти найкращу відповідність доступних функцій.

У цьому випадку, gogo(int*)він знаходиться (окремо) в області виведеного класу, і оскільки немає стандартного перетворення з int в int *, пошук не вдається.

Рішення полягає в тому, щоб ввести базові декларації за допомогою використовуючої декларації у класі Отримані:

using Base::gogo;

... дозволить правилам пошуку імен знайти всіх кандидатів, і, таким чином, рішення перевантаження буде тривати так, як ви очікували.


10
ОП: "Чому перекрита функція у похідному класі приховує інші перевантаження базового класу?" Ця відповідь: "Тому що це робить".
Річард Вольф

12

Це "За задумом". У розділі C ++ роздільна здатність перевантаження для цього типу методу працює наступним чином.

  • Починаючи з типу посилання, а потім переходячи до базового типу, знайдіть перший тип, який має метод з назвою "gogo"
  • Розглядаючи лише методи, названі "gogo" для цього типу, знаходять відповідне перевантаження

Оскільки в Derived немає функції узгодження під назвою "gogo", роздільна здатність перевантаження не вдається.


2

Приховування імен має сенс, оскільки це запобігає двозначності у вирішенні імен.

Розглянемо цей код:

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

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

Derived dobj;

Якщо Base::func(float)це не було заховано Derived::func(double)в Derived, ми би викликали функцію базового класу під час виклику dobj.func(0.f), навіть незважаючи на те, що флоат може бути підвищений до подвійного.

Довідка: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.