сокрытие имени и хрупкая базовая проблема - PullRequest
15 голосов
/ 08 мая 2011

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

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

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(), потому что это перегрузка.

Ответы [ 3 ]

7 голосов
/ 08 мая 2011

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

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 голосов
/ 20 августа 2012

In Дизайн и развитие C ++ , Бьярн Страуструп Аддисон-Веслей, 1994, раздел 3.5.3, с. 77, 78, BS объясняет, что правило, согласно которому имя в производном классе скрывает все определениято же имя в его базовых классах старое и восходит к C с классами.Когда он был представлен, BS рассматривал его как очевидное следствие правил области видимости (то же самое для вложенных блоков кода или вложенных пространств имен - даже если пространство имен было введено после).Желательность его взаимодействия с правилами перегрузки (перегруженный набор не содержит функции, определенной в базовых классах, ни во включающих блоках - теперь безвреден, поскольку объявление функций в блоке является старомодным, - ни во вложенных пространствах имен, где иногда возникает проблемаТакже обсуждался вопрос о том, что 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*)
}

Затем BS продолжает

Оглядываясь назад, я подозреваю, что правила перегрузки, введенные в 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); }

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

2 голосов
/ 08 мая 2011

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

Предположим, у нас есть иерархия из 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, независимо от того, какой истинный (во время выполнения) тип имеет 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 в базовом классе, особенно когда неизвестный код неизвестен (библиотеки доставляются клиентам).

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

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