Скрыть имя и хрупкую базовую проблему
Я видел, как он заявил, что С++ имеет имя, скрывающееся в целях уменьшения проблемы хрупкого базового класса. Тем не менее, я определенно не вижу, как это помогает. Если базовый класс вводит функцию или перегрузку, которая ранее не существовала, она может конфликтовать с теми, которые были введены производным классом, или неквалифицированными вызовами глобальных функций или функций-членов, но я не вижу, как это отличается для перегрузок, Почему перегрузки виртуальных функций должны обрабатываться по-разному, ну, и любая другая функция?
Изменить: Позвольте мне показать вам немного больше, о чем я говорю.
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()
, потому что это перегрузка.
Ответы
Ответ 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 С++ будут иметь здесь значение.