Правило 33: Не скрывайте унаследованные имена
Правило 33: Не скрывайте унаследованные имена
Шекспир много размышлял об именах. Он писал: «Что в имени тебе? Роза пахнет розой, хоть розой назови ее, хоть нет». И еще писал бард: «Кто доброе мое похитит имя, несчастным сделает меня вовек…» Правильно. И это заставляет нас обратить взор на унаследованные имена в C++.
Вообще-то эта тема относится не столько к наследованию, сколько к областям видимости. Все мы знаем, что в таком коде:
int x; // глобальная переменная
void someFunc()
{
double x; // локальная переменная
std::cin >> x; // прочитать новое значение локальной переменной x
}
имя x в предложении считывания относится к локальной, а не к глобальной переменной, потому что имена во вложенной области видимости скрывают («затеняют») имена из внешних областей. Мы можем представить эту ситуацию визуально:
Когда компилятор встречает имя x внутри функции someFunc, он смотрит, определено ли что-то с таким именем в локальной области видимости. Если да, то объемлющие области видимости не просматриваются. В данном случае имя x в функции someFunc принадлежит переменной типа double, а глобальная переменная с тем же именем x имеет тип int, но это несущественно. Правила сокрытия имен в C++ предназначены для одной-единственной цели: скрывать имена. Относятся ли одинаковые имена к объектам одного или разных типов, не имеет значения. В нашем примере переменная x типа double скрывает переменную x типа int.
Вернемся к наследованию. Мы знаем, что когда находимся внутри функции-члена производного класса и ссылаемся на что-то из базового класса (например, функцию-член, typedef или член данных), компилятор сможет найти то, на что мы ссылаемся, потому что производные классы наследуют свойства, объявленные в базовых классах. Механизм основан на том, что область видимости производного класса вложена в область видимости базового класса. Например:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void mf3();
...
};
class Derived: public Base {
public:
virtual void mf1()
void mf4();
...
};
В этом примере встречаются как открытые, так и закрытые имена, как имена членов данных, так и функций-членов. Одна из функций-членов – чисто виртуальная, другая – просто виртуальная, а третья – невиртуальная. Это я к тому, что мы говорим именно об именах, а не о чем-то другом. Я мог бы включить в пример еще имена типов, например перечислений, вложенных классов и typedef. В данном контексте важно лишь то, что все это имена. Что они именуют – несущественно. В примере используется одиночное наследование, но, поняв, что происходит при одиночном наследовании, легко будет разобраться и в том, как C++ ведет себя при множественном наследовании.
Предположим, что функция-член mf4 в производном классе реализована примерно так:
void Derived::mf4()
{
...
mf2();
...
}
Когда компилятор видит имя mf2, он должен понять, на что оно ссылается. Для этого в различных областях видимости производится поиск имени mf2. Сначала оно ищется в локальной области видимости (то есть внутри mf4), но там такого имени нет. Тогда просматривается объемлющая область видимости, то есть область видимости класса Derived. И здесь такое имя отсутствует, поэтому компилятор переходит к следующей область видимости, которой является базовый класс. И находит там нечто по имени mf2, после чего поиск завершается. Если бы mf2 не было и в классе Base, то поиск продолжился бы сначала в пространстве имен, содержащем Base, если таковое имеется, и, наконец, в глобальной области видимости.
Данное мной описание правильно, хотя и исчерпывает всю сложность процесса поиска имен в C++. Наша цель, однако, не в том, чтобы узнать о поиске имен столько, чтобы самостоятельно написать компилятор. Достаточно будет, если мы сумеем избежать неприятных сюрпризов, а для этого изложенной информации должно хватить.
Снова вернемся к предыдущему примеру, но на этот раз перегрузим функции mf1 и mf3, а также добавим версию mf3 в класс Derived. Как объясняется в правиле 36, перегрузка mf3 в производном классе Derived (когда наследуется невиртуальная функция) сама по себе подозрительна, но чтобы лучше разобраться с видимостью имен, закроем на это глаза.
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
virtual void mf1()
void mf3();
void mf4();
...
};
Этот код приводит к поведению, которое удивит любого программиста C++, впервые столкнувшегося с ним. Основанное на областях видимости правило сокрытия имен никуда не делось, поэтому все функции с именами mf1 и mf3 в базовом классе окажутся скрыты одноименными функциями в производном классе. С точки зрения поиска имен, Base::mf1 и Base::mf3 более не наследуются классом Derived!
Derived d;
int x;
...
d.mf1(); // правильно, вызывается Derived::mf1
d.mf1(x); // ошибка! Derived::mf1 скрывает Base::mf1
d.mf2(); // правильно, вызывается Base::mf2
d.mf3(); // правильно, вызывается Derived::mf3
d.mf3(x); // ошибка! Derived::mf3 скрывает Base::mf3
Как видите, это касается даже тех случаев, когда функции в базовом и производном классах принимают параметры разных типов, независимо от того, идет ли речь о виртуальных или невиртуальных функциях. И точно так же, как в нашем первом примере double x внутри функции someFunc скрывает int x из глобального контекста, так и здесь функция mf3 в классе Derived скрывает функцию mf3 из класса Base, которая имеет другой тип.
Обоснование такого поведения в том, что оно не дает нечаянно унаследовать перегруженные функции из базового класса, расположенного много выше в иерархии наследования, упрятанной в библиотеке или каркасе приложения. К сожалению, обычно вы хотите унаследовать перегруженные функции. Фактически если вы используете открытое наследование и не наследуете перегруженные функций, то нарушаете семантику отношения «является» между базовым и производным классами, которое в правиле 32 провозглашено фундаментальным принципом открытого наследования. То есть это тот случай, когда вы почти всегда хотите обойти принятое в C++ по умолчанию правило сокрытия имен.
Это можно сделать с помощью using-объявлений:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived: public Base {
public:
using Base::mf1; // обеспечить видимость всех (открытых) имен
using Base::mf3; // mf1 и mf3 из класса Base в классе Derived
virtual void mf1()
void mf3();
void mf4();
...
};
Теперь наследование будет работать, как и ожидается.
Derived d;
int x;
...
d.mf1(); // по-прежнему правильно, вызывается Derived::mf1
d.mf1(x); // теперь правильно, вызывается Base::mf1
d.mf2(); // по-прежнему правильно, вызывается Base::mf2
d.mf3(); // по-прежнему правильно, вызывается Derived::mf3
d.mf3(x); // теперь правильно, вызывается Base::mf3
Это означает, что если вы наследуете базовому классу с перегруженными функциями и хотите переопределить только некоторые из них, то должны включить using-объявление для каждого имени, иначе оно будет скрыто.
Можно представить себе ситуацию, когда вы не хотите наследовать все функции из базовых классов. При открытом наследовании такое никогда не должно происходить, так как это противоречит смыслу отношения «является» между базовым классом и производным от него. Вот почему using-объявление находится в секции public объявления производного класса; имена, которые открыты в базовом классе, должны оставаться открытыми и в открыто унаследованном от него. Но при закрытом наследовании (см. правило 39) такое желание иногда осмыслено. Например, предположим, что класс Derived закрыто наследует классу Base, и единственная версия mfl, которую Derived хочет унаследовать, – это та, что не принимает параметров. Using-объявление в этом случае не поможет, поскольку оно делает видимыми в производном классе все унаследованные функции с заданным именем. Здесь требуется другая техника – простая перенаправляющая функция:
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
... // как раньше
};
class Derived: private Base {
public:
virtual void mf1() // перенаправляющая функция
{ Base::mf1();} // неявно встроена (см. правило 30)
...
};
...
Derived d;
Int x;
d.mf1(); // правильно, вызывается Derived::mf1
d.mf1(x); // ошибка! Base::mf1 скрыта
Другое применение встроенных перенаправляющих функций – обойти дефект в тех устаревших компиляторах, которые не поддерживают using-объявления для импорта унаследованных имен в область видимости производного класса.
Это все, что можно сказать о наследовании и сокрытии имен. Впрочем, когда наследование сочетается с шаблонами, возникает совсем другой вариант проблемы «сокрытия унаследованных имен». Все подробности, касающиеся шаблонов, см. в правиле 43.
Что следует помнить
• Имена в производных классах скрывают имена из базовых классов. При открытом наследовании это всегда нежелательно.
• Чтобы сделать скрытые имена видимыми, используйте using-объявления либо перенаправляющие функции.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКДанный текст является ознакомительным фрагментом.
Читайте также
Имена
Имена В именах нельзя использовать символы разных регистров. Назвать переменную именем idx, или даже i — это очень хорошо, но при условии, что будет понятно назначение этой переменной. Слишком хитрые имена, такие как theLoopIndex, недопустимы. Так называемая "венгерская запись"
17.8. Унаследованные сетевые функции
17.8. Унаследованные сетевые функции В данный момент действует множество библиотечных функций, относящихся к работе сетей TCP/IP, которые нельзя применять в новых приложениях. Однако они широко используются в существующих IPv4-программах. В связи с этим они рассматриваются
Унаследованные возможности
Унаследованные возможности Рассмотрим возможности, которые уже были реализованы в предыдущих версиях Windows, начиная с Windows 2000 и заканчивая Windows XP Service Pack2, но в Windows Vista – модифицированы и улучшены.Ознакомимся со средством переноса пользовательских настроек. Встроенная в
Имена объектов
Имена объектов В System/38 объекты были как в ОС, так и в MI. Определением этих объектов и выбором имен для них занимались две разные группы. Одна разрабатывала объекты CPF, (которая в AS/400 была переименована в OS/400[ 42 ]), другая — разрабатывала набор команд и системные объекты
Идентификаторы и имена в IPC
Идентификаторы и имена в IPC Как было показано, отсутствие имен у каналов делает их недоступными для независимых процессов. Этот недостаток устранен у FIFO, которые имеют имена. Другие средства межпроцессного взаимодействия, являющиеся более сложными, требуют
Имена файлов
Имена файлов Как мы уже видели, ни метаданные, ни тем более блоки хранения данных, не содержат имени файла. Имя файла хранится в файлах специального типа — каталогах. Такой подход позволяет любому файлу, т. е. фактическим данным, иметь теоретически неограниченное число
2.2. Имена IPC
2.2. Имена IPC В табл. 1.2 мы отметили, что три типа IPC стандарта Posix имеют идентификаторы (имена), соответствующие этому стандарту. Имя IPC передается в качестве первого аргумента одной из трех функций: mq_open, sem_open и shm_open, причем оно не обязательно должно соответствовать реальному
30. Имена переменных
30. Имена переменных Имя (идентификатор) включает в себя последовательность букв и цифр. Первый символ должен являться буквой. Знак подчерка считается буквой. C++ не ограничивает число символов в имени, но определенные части реализации находятся вне ведения автора
Имена, используемые MFC
Имена, используемые MFC Библиотека классов содержит огромное количество классов, структур, констант и т. д. Чтобы сделать исходный текст приложений MFC более легким для понимания, принято использовать ряд соглашений для используемых имен и комментариев.Названия всех
2.2 Имена
2.2 Имена Имя (идентификатор) состоит из последовательности букв и цифр. Первый символ должен быть буквой. Символ подчерка _ считается буквой. С++ не налагает ограничений на число символов в имени, но некоторые части реализации находятся вне ведения автора компилятора (в
4. Имена и Типы
4. Имена и Типы Имя обозначает(денотирует) объект, функцию, тип, значние или метку. Имя вводится в программе описанием (#8). Имя может использоваться только внутри области текста программы, называемой его областью видимости. Имя имеет тип, определящий его использование.
7.3. Имена дисков
7.3. Имена дисков В любом современном компьютере имеется несколько носителей данных, например дисковод для дискет, CD-ROM и жесткий диск. У каждого носителя есть свое имя. Оно позволяет уточнить, на каком диске находится тот или иной файл. Имя диска — это буква латинского
У6.5 Имена
У6.5 Имена Разработайте абстрактный тип данных ИМЯ, в котором учитывались бы различные компоненты полного имени
Играем в имена
Играем в имена Смена имен подчеркивает важность именования - как компонентов, так и классов - в практике ОО-разработки ПО. Формально, класс - это отображение имен компонентов в сами компоненты. Компоненты известны остальному миру благодаря именам.В последней лекции будет