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


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

#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

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

4 192

4 ответа:

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

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

такое поведение считается нежелательно, когда язык был разработан. В качестве лучшего подхода было решено следовать спецификации "скрытие имени", что означает, что каждый класс начинается с" чистого листа " в отношении каждого имени метода, которое он объявляет. Чтобы переопределить это поведение, от пользователя требуется явное действие: первоначально повторное объявление унаследованных методов (в настоящее время устаревших), теперь явное использование using-declaration.

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

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

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

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

using Base::gogo;

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

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

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

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

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

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

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

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

Derived dobj;

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

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