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

StackOverflow https://stackoverflow.com/questions/1628768

  •  06-07-2019
  •  | 
  •  

Вопрос

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

#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++ выглядит не очень хорошо.Не полиморфен.

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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) в Derived, мы вызовем функцию базового класса при вызове dobj.func (0.f) , хотя число с плавающей точкой может быть увеличено до двойного.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top