Почему переопределении функции в производном классе скрывает другие перегрузки базового класса?
рассмотрим код :
#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/