Скрыть имя и хрупкую базовую проблему - программирование

Скрыть имя и хрупкую базовую проблему

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

Изменить: Позвольте мне показать вам немного больше, о чем я говорю.

struct base {
    virtual void foo();
    virtual void foo(int);
    virtual void bar();
    virtual ~base();
};
struct derived : base {
    virtual void foo();
};

int main() {
    derived d;
    d.foo(1); // Error- foo(int) is hidden
    d.bar(); // Fine- calls base::bar()
}

Здесь foo(int) обрабатывается по-разному с bar(), потому что это перегрузка.

4b9b3361

Ответ 1

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

struct A {};

struct B : public A
{
    void f(float);
};

void do_stuff()
{
    B b;
    b.f(3);
}

Вызов функции в do_stuff вызывает B::f(float).

Теперь предположим, что кто-то модифицирует базовый класс и добавляет функцию void f(int);. Без скрытия это было бы лучшим совпадением для аргумента функции в main; вы либо изменили поведение do_stuff (если новая функция является общедоступной), либо вызвали ошибку компиляции (если она закрыта), не изменяя ни do_stuff, ни какие-либо из ее прямых зависимостей. С укрытием вы не изменили свое поведение, и такое нарушение возможно только в том случае, если вы явно отключите скрытие с объявлением using.

Ответ 2

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

Предположим, что у нас есть иерархия из 3 слоев:

struct Base {};

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

struct Top: Derived { void foo(int i); }; // hides Derived::foo

Когда я пишу:

void bar(Derived& d) { d.foo(3); }

вызов статически разрешен на Derived::foo, независимо от того, какой тип true (runtime), который d может иметь.

Однако, если я тогда введу virtual void foo(int i); в Base, тогда все изменится. Внезапно Derived::foo и Top::foo становятся переопределениями вместо простой перегрузки, которая скрывала имя в соответствующем базовом классе.

Это означает, что d.foo(3); теперь статически ставится не непосредственно на вызов метода, а на виртуальную отправку.

Поэтому Top top; bar(top) вызовет Top::foo (через виртуальную отправку), где он ранее назывался Derived::foo.

Это может быть нежелательно. Его можно было бы зафиксировать, явно присвоив вызов d.Derived::foo(3);, но он наверняка является неудачным побочным эффектом.

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

Примечание: если мы удалим Top, то это совершенно прекрасно, чтобы представить новый виртуальный метод, поскольку все старые вызовы уже обрабатывались Derived:: foo в любом случае, и, таким образом, только новый код может быть затронут

Это нужно иметь в виду, но при введении новых методов virtual в базовый класс, особенно когда поврежденный код неизвестен (библиотеки доставляются клиентам).

Обратите внимание, что С++ 0x имеет атрибут override, чтобы проверить, что метод действительно является переопределением базового виртуального; в то время как он не решает ближайшей проблемы, в будущем мы могли бы представить, что компиляторы имеют предупреждение для "случайных" переопределений (т.е. переопределения не помечены как таковые), и в этом случае такая проблема может быть обнаружена во время компиляции после введения виртуальный метод.

Ответ 3

В дизайне и эволюции С++, Bjarne Stroustrup Addison-Weslay, 1994, раздел 3.5.3 pp. 77, 78, B.S. объясняет, что правило, по которому имя в производном классе скрывает все определения одного и того же имени в его базовых классах, является старым и датируется с C с классами. Когда он был введен, Б.С. считал это очевидным следствием правил определения области видимости (он одинаковый для вложенных блоков кода или вложенных пространств имен - даже если после этого было введено пространство имен). Желательность его взаимодействия с правилами перегрузки (перегруженный набор не содержит функции, определенной в базовых классах, а также в закрывающих блоках - теперь безвреден, поскольку объявление функций в блоке старомодно, а также в пространстве имен, где проблема иногда забастовки) обсуждался, до такой степени, что g++ реализовал альтернативные правила, позволяющие перегрузку, и BS утверждал, что действующее правило помогает предотвращать ошибки в таких ситуациях, как (вдохновленные настоящими живыми проблемами с g++)

class X {
   int x;
public:
   virtual void copy(X* p) { x = p->x; }
};

class XX: public X {
   int xx;
public:
   virtual void copy(XX* p) { xx = p->xx; X::copy(p); }
};

void f(X a, XX b) 
{
   a.copy(&b); // ok: copy X part of b
   b.copy(&a); // error: copy(X*) is hidden by copy(XX*)
}

Затем Б.С. продолжается

В ретроспективе я подозреваю, что правила перегрузки, введенные в 2.0, могли бы справиться с этим случаем. Рассмотрим вызов b.copy(&a). Переменная b является точным совпадением типов для неявного аргумента XX::copy, но для стандартного преобразования требуется соответствие X::copy. Переменная a, с другой стороны, является точным соответствием для явного аргумента X::copy, но требует стандартного преобразования в соответствии с XX:copy. Таким образом, если перегрузка была разрешена, вызов был бы ошибкой, потому что это неоднозначно.

Но я не вижу, где двусмысленность. Мне кажется, что Б.С. что &a не может быть неявно преобразован в XX*, и поэтому рассматривается только X::copy.

Действительно пытается с помощью бесплатных (друзей) функций

void copy(X* t, X* p) { t->x = p->x; }
void copy(XX* t, XX* p) { t-xx = p->xx; copy((X*)t, (X*)p); }

Я не получаю ошибку двусмысленности с текущими компиляторами, и я не вижу, как правила в Справочном руководстве Annotated С++ будут иметь здесь значение.