Причина скрытия функции-члена С++ - программирование

Причина скрытия функции-члена С++

Возможный дубликат:
имя скрывается и хрупкая базовая проблема

Я знаком с правилами, связанными со скрытием функции-члена. В принципе, производный класс с функцией, которая имеет то же имя, что и функция базового класса, фактически не перегружает функцию базового класса - она ​​полностью скрывает ее.

struct Base
{
    void foo(int x) const
    {

    }
};

struct Derived : public Base
{
    void foo(const std::string& s) { }
};


int main()
{
    Derived d;
    d.foo("abc");
    d.foo(123); // Will not compile! Base::foo is hidden!
}

Итак, вы можете обойти это с помощью объявления using. Но мой вопрос: в чем причина скрытия функции базового класса? Является ли это "особенностью" или просто "ошибкой" комитета по стандартам? Есть ли какая-то техническая причина, по которой компилятор не может выглядеть в классе Base для соответствия перегрузок, если он не находит соответствия для d.foo(123)?

4b9b3361

Ответ 1

Поиск имени работает, просматривая текущую область для сопоставления имен, если ничего не найдено, то она отображается в охватывающей области, если ничего не найдено, она отображается в охватывающей области и т.д. до достижения глобального пространства имен.

Это не относится к классам, вы получаете то же имя, что и здесь:

#include <iostream>

namespace outer
{
  void foo(char c) { std::cout << "outer\n"; }

  namespace inner
  {
    void foo(int i) { std::cout << "inner\n"; }

    void bar() { foo('c'); }
  }
}

int main()
{
  outer::inner::bar();
}

Хотя outer::foo(char) является лучшим совпадением для вызова foo('c') поиска имен после прекращения поиска outer::inner::foo(int) (т.е. outer::foo(char)), и поэтому программа печатает inner.

Если имя функции-члена не было скрыто, это означало бы, что поиск имени в области класса вел себя по-разному с неклассической областью, что было бы непоследовательным и запутанным, а С++ еще труднее изучить.

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

Ответ 2

Является ли это "особенностью" или просто "ошибкой" комитета по стандартам?

Это определенно не ошибка, поскольку она четко оговорена в стандарте. Это особенность.

Есть ли какая-то техническая причина, почему компилятор не может выглядеть в базовом классе для соответствия перегрузок, когда он не находит соответствия для d.foo(123)?

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

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

Если кто-то из комитета приходит с ответом, я думаю, мы можем только догадываться. В принципе, было два варианта:

  • если я объявляю функцию с тем же именем в производном классе, сохраняйте функции базового класса с тем же именем, которые доступны непосредственно через производный класс
  • не

Это можно было бы определить, перевернув монету (... хорошо, может быть, нет).

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

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

Ответ 3

Я могу только предложить, чтобы это решение было сделано, чтобы сделать вещи проще.

Представьте, что производная функция перегрузит базовую. Затем должен ли следующий код генерировать ошибку компиляции или использовать функцию Derived?

struct Base
{
private:
    void foo(float);
}

struct Derived: public Base
{
public:
    void foo(int);
}

int main()
{
    Derived d;
    d.foo(5.0f);
}

В соответствии с существующим поведением перегрузок это должно вызвать ошибку.

Теперь представьте, что в первой версии Base не было foo(float). Во второй версии появляется. Теперь изменение реализации базового класса нарушает интерфейс производного.

Если вы являетесь разработчиком Derived и не можете влиять на разработчиков Base, и многие клиенты используют ваш интерфейс, вы сейчас находитесь в плохом состоянии.

Ответ 4

Я считаю, что @Lol4t0 в значительной степени верен, но я бы сказал гораздо сильнее. Если вы допустили это, у вас получится две возможности: либо внести множество других изменений практически на весь язык, либо в итоге вы получите что-то почти полностью сломанное.

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

Чтобы сделать эту работу даже неплохой, вы в значительной степени должны изменить это, чтобы сначала проверить доступ и только добавить доступные функции к набору перегрузки. При этом, по крайней мере, пример в ответе @Lol4t0 может продолжать компиляцию, потому что Base::foo никогда не будет добавлен в набор перегрузки.

Это все равно означает, что добавление к интерфейсу базового класса может вызвать серьезные проблемы. Если Base изначально не содержал foo, а публичный foo был добавлен, то вызов в main to d.foo() внезапно сделал бы что-то совершенно другое, и (опять же) оно было бы полностью вне контроль над тем, кто написал Derived.

Чтобы вылечить это, вы должны были бы сделать довольно фундаментальное изменение в правилах: запретить неявные преобразования аргументов функции. Наряду с этим вы бы изменили разрешение перегрузки, поэтому в случае галстука наиболее производная/самая локальная версия функции была предпочтительна в отношении менее производной/внешней области. С помощью этих правил вызов d.foo(5.0) никогда не сможет решить Derived::foo(int) в первую очередь.

Это, однако, оставило бы только две возможности: либо вызов бесплатных функций имел бы разные правила, чем вызовы функций-членов (неявные преобразования допускались только для бесплатных функций), либо вся совместимость с C была бы полностью отброшена (т.е. также запретить неявные преобразования во всех аргументах функций, что приведет к разрушению огромного количества существующего кода).

Подводя итог: чтобы изменить это, полностью не сломав язык, вам придется внести немало других изменений. Почти наверняка можно было бы создать язык, который работал бы таким образом, но к тому времени, когда вы закончите, это не будет С++ с одним незначительным изменением - это будет совсем другой язык, который не очень похож на С++ или C, или многое другое.