Почему переопределении функции в производном классе скрывает другие перегрузки базового класса?
рассмотрим код :
#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 ответа:
судя по формулировке вашего вопроса (Вы использовали слово "скрывать"), вы уже знаете, что здесь происходит. Явление называется "сокрытие имени". По какой-то причине, каждый раз, когда кто-то задает вопрос о почему скрытие имени происходит, люди, которые отвечают, либо говорят, что это называется "скрытие имени" и объясняют, как это работает (что вы, вероятно, уже знаете), либо объясняют, как его переопределить (о чем вы никогда не спрашивали), но никто, похоже, не заботится о том, чтобы обратиться к фактическому " почему" вопрос.
решение, обоснование скрытия имени, Т. е. почему он фактически был разработан в 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/