Почему переопределенная функция в производном классе скрывает другие перегрузки базового класса?

Рассмотрим код:

#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++ -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

Здесь функция Derived class затмевает все функции с тем же именем (не подписи) в базовом классе. Так или иначе, это поведение С++ не выглядит ОК. Не полиморфный.

Ответ 1

Судя по формулировке вашего вопроса (вы использовали слово "скрыть"), вы уже знаете, что здесь происходит. Это явление называется "сокрытие имени". По какой-то причине каждый раз, когда кто-то задает вопрос о том, почему происходит скрытие имени, люди, которые отвечают, говорят, что это называется "скрытие имени" и объясняют, как это работает (что вы, вероятно, уже знаете), или объясните, как его переопределить (что вы никогда не спрашивали), но никто, кажется, не заботится об актуальном вопросе "почему".

Решение, обоснование скрытия имени, то есть, почему оно фактически было сконструировано в 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) вызовы 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 между классами. Это правда, но, по-видимому, тогда было решено, что в конечном итоге сокрытие имен окажется меньшим злом.

Ответ 2

В правилах разрешения имен указано, что поиск имен останавливается в первой области, в которой найдено совпадающее имя. В этот момент правила разрешения перегрузки срабатывают, чтобы найти наилучшее соответствие доступных функций.

В этом случае gogo(int*) находится (отдельно) в области класса Derived, и поскольку стандартного преобразования из int в int * нет, поиск не выполняется.

Решение состоит в том, чтобы принести объявления Base через декларацию using в классе Derived:

using Base::gogo;

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

Ответ 3

Это "По дизайну". В разрешении перегрузки С++ для этого типа метода работает следующим образом.

  • Начиная с типа ссылки, а затем перейдя к базовому типу, найдите первый тип, у которого есть метод с именем "gogo"
  • Учитывая только методы с именем "gogo" в этом типе, найдите соответствующую перегрузку

Так как Derived не имеет соответствующей функции с именем "gogo" , разрешение перегрузки выходит из строя.

Ответ 4

Сокрытие имени имеет смысл, поскольку предотвращает неоднозначность разрешения имен.

Рассмотрим этот код:

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), хотя float можно было бы продвигать до двойной,

Ссылка: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/