it-swarm.com.ru

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

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

#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: в функции `int main () ': 
 test.cpp: 31: ошибка: нет соответствия функция для вызова Derived :: gogo (int) '
 test.cpp: 21: примечание: кандидаты: виртуальные пустые Derived :: gogo (int *) 
 test.cpp: 33: 2: предупреждение: нет новой строки в конце файла 
> Код выхода: 1 

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

207
Aman Aggarwal

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

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

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

386
AnT

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

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

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

using Base::gogo;

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

43
Drew Hall

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

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

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

13
JaredPar

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

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

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/

2
Sandeep Singh