Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 79
Текст из файла (страница 79)
*/ чШыа1 чо1с) г(); ); с1авв Ч ( /* ... */ чътсыа1 чо1в( д(); ); с1авв Х ( /* нет виртуальных функций */ ); с1авв р: ры)»1)с в, рцЫ1с ч1гвва1 ч, ри)в1(с ч1гсиа1 х ( а те, с которыми ассоциирована информация о типе, так что их тип можно определить во время исполнения почти независимо от контекста; щ объекты, для которых это утверждение неверно. Почему? Мы нс можем илентифицировать во время исполнения тип объектов встроенных типов (напрымер, 1пс или с)оц)э1е), поскольку это привело бы к неприемлемому расходу времени и памяти, а также породило проблемы совместимости с моделью размещения объектов в памяти. То же самое относится и к объектам простых классов, и к структурам в стиле С.
Следовательно, первая граница проходит между объектами классов с виртуальными функциями и классов без таковых. Для первых можно легко предоставить идентификацию типа во время исполнения, для вторых — нет. Кроме того, классы с виртуальными функциями часто называют полиморфными, и только пх объектамп можно безопасно манипулировать через базовый класс. Под словом «безопасно» я здесь понимаю то, что язык гарантирует использование объекта лишь в соответствии с его типом.
Разумеется, некоторые программисты могут продемонстрировать, что в особых случаях такого рода манипуляции с неполцморфными классами не нарушают систему типов. Поэтому с учетом задач программирования представляется естественным обеспечить идентификацию типа во время исполнения только для полиморфных типов. Поддержка КТТ! для других типов сводится просто к ветвлению по полю типа. Конечно, язык не должен препятствовать и такому стилю программирования, но нет необходимости усложнять язык только для того, чтобы поддержать это явно. Практика показывает, что предоставление КТТ[ исключительно для полиморфных типов вполне приемлемо.
Однако пользователи часто не понимают, какие объекты полиморфны и можно ли применять к ним динамическое приведение типов. К счастью, компилятор выдаст сообщение об ошибке, если догадка программиста окажется неверной. Я долго и настойчиво искал приемлемый способ явно сказать: «Этот класс поддерживает КТТ1 (вне зависимости от того, есть в нем виртуальные функции или нет)», — но не нашел такого, на который стоило бы тратить силы.
Идентификация типа во время исполнения авияяяяящ чо1б ( в* о* о* о(оа а) рь = р1 р2 ьб; (р*)рьи дупашьс сазс<Р*>(рЬ)," правильно; не проверяется // правильно: проверяется во время // исполнения ап; (Р")рч; рч = рз // ошибка: приведение из виртуального // базового класса невозможно // правильно: проверяется во время // исполнения Р* р4 = йупаш1с санс<в*>(рч)) х* рх = ас); Р* р5 = (о*)рх; // ошибка: приведение из виртуального // базового класса невозможно // ошибка: прмведение из // неполиморфного типа невозможно О* рб = аупаш1с санс<о*>(рх); Разумеется, такое приведение можно выполнить лишь в том случае, если тип производного класса определяется однозначно, 14.2.3.
Лравильное и неправильное использование !гТТ! Пользоваться механизмом явной идентификации типа во время исполнения стоит только при необходимости. Статическая проверка 1во время компиляции) безопаснее, связана с меньшими затратами и в тех случаях, когда ее хватает, позволяет получать лучше структурированные программы. Например, КТТ[ можно использовать для написания явно видимых операторов переключения: // неправильное пргв«енение информации о типе: чо1г) гогаге(сопвг Бьареь г) ( 11 (суре1с) (г) == гуре1«) (С1гс1е) ) ( // ничего не делать е1ве 11 (гуре1«)(г) == суревз(тгьапр1е)) ( // повернуть треугольник е1ве 1г (гуре1с)(г) == суреы (зс(ваге)) ( // повернуть квадрат ) // ) Я слышал, будто о таком стиле говорили, что «он соединяет синтаксическое изящество С++ с эффективностью Бгпа)!гаИг», но это излишняя любезность.
К сожалению, с помощью этого кода нельзя корректно обработать классы, производные от встречающихся в нем, следовательно, при добавлении в программу нового класса его придется модифицировать. Приведение типов КИИИИИИВ В таком случае, лучше пользоваться виртуальными функциями. Мой опыт работы с подобным кодом в 5(шц!а и послужил той причиной, по которой в С-и- первоначально не были включены средства для идентификации типа во время исполнения (см. раздел 3.5).
У кп(огих пользователей, имеющих опыт работы с такими языками, как С, Рааса!, Мог)п!а-2, А((а и т.д., возникает почти непреодолимое желание организовать код в виде набора предложений ям(с сЬ. Как правило, этого делать пс следует. Заметьте, пожалуйста, что, хоть комитет по стандартизации и одобрил механизм КТТ1 в С++, он реализован не в впдс переключения по тину (как предложение 1)чБ рЕСТ в 3(шп1а).
Видимо, не стоит напрямую поддерживать такую модель организации программы. Примеров, где это оправдано, гораздо меньше, чем поначалу кажется многим программистам. Потом же оказывается, что для реорганизации нужно приложить слишком много усилий. Зачастую КТТ1 используется правильно, когда некий сервисный код выражен в терминах одного класса, а пользователь намерен расширить его функциональность с помощью наследования. Примером может служить класс с(фа1од Ьох из раздела 14.2.1. Если пользователь хочет и может модифицировать определения библиотечных классов, скажем, того же с(фа1од Ьох, то к КТТ1 можно н не прибегать, в ином случае это средство потребуется обязательно. Но даже если пользователь готов модифицировать базовые классы, на этом пути могут возникнуть определенные трудности. Например, может оказаться необходимым ввести заглушки вместо некоторых виртуальных функций, например де с в" гфпд ( ) в классах, для которых такие функции не нужны или бессмысленны.
Данная проблема обсуждается в книге [2п(1, ф13.13.6! подзаголовком «Гас! пгегГасевв (етолстые» интерфейсы). О применении КТТ1 для реализации простой системы ввода/вывода объектов говорится в разделе ! 4.2.7. У людей, ранее работавших с языками, где интенсивно используется динамическая проверка типов (например, Зта!!га(!с), возникает желание применить КТТ! в сочетании с излишне обобщенными типами. Например: // неправильное применение информации о типе: с1авя ОЬ)есв ( /* ... */ )~ с1аяя сопла!пег : риЫгс ОЬ)есс ( рцЫгс: чоьд рис(ОЬ)есс*); ОЬ)есс* дес(); // с1авя БЫр: рпЫ1с ОЬ)еос ( /* ...
*/ БЫр* Г(яи!р* ря, Оопса1пег' с) ( с->рос(рв)) Идентификация типа во время исполнения ДффЯЯЩЯЩ оп)есс* р = с->пес(); (ядтр* с( = дупашьс савп<яп1р*>(р)) // проверка во время // выполнения гесптп с(," // сделать что-то еше (обычно — обработать ошибку) ) Здесь класс о)зз есс совершенно не нужен. Он слишком общий, так как не соответствует ни одному из понятий предметной области и заставляет программиста оперировать на более низком уровне абстракции, чем необходимо. Такого рода задачи лучше рецшть с помощью шаблонов контейнеров, содержащих указатели только одного типа: сешр1асе<с1авв т> с1авв сопсавпет ( рпь11с: уоЫ рпп(Т*); Т* Чек()1 // Янур* Гр(ЯПтр* рв, Соппатпет<ЯП1р>* с) ( с->рос(рв)) // теспгп с->пес(); В сочетании с виртуальными функциями этот прием подходит в большинстве случаев. 14.2.4.
Зачем давать «опасные средства» Итак, если я точно предвидел, что КТТ1 будут использовать ненадлежащим образом, то зачем вообще спроектировал этот механизм и боролся за его одобрение? Хорошие программы — это продукт хорошего образования, удачного проектирования, адекватного тестирования и т.д., а не наличия в языке средств, которые предположительно должны использоваться только <правильно>. Употребить во вред можно любое средство, поэтому вопрос нс в том, можно ли чем-то воспользоваться неправильно (можно!) или будут ли этим неправильно пользоваться (будут!). Вопрос в том, необходимо ли данное средство при условии его правильного употребления настолько, чтобы оправдать затраты на его реализацию, удастся ли смоделировать его с помощью других средств языка и реально ли с помощью обучения обратить во благо неправильное использование.
Размышляя некоторое время над КТТ1, я пришел к выводу, что перед нами обычная проблема стандартизации: о в большинстве крупных библиотек КТТ1 поддерживается; и как правило, средство предоставляется в таком виде, который требует от пользователя написания громоздкого дополнительного кода, в результате чего весьма вероятны ошибки; БИИИВИИВ Приведение типов гз реализации в разных библиотеках несовместимы между собой; о обычно реализация является недостаточно общей; гз в большинстве случаев КТТ1 выглядит как чпривлекательная штучка», которой так и хочется воспользоваться, а не как опасное устройство, к которому надо прибегать в крайнем случае; з в любой крупной библиотеке, похоже, существует немного случаев, где применение КТТ1 необходимо настолько, что без нее библиотека либо вообще не могла бы предоставить какую-нибудь возможность, либо прелоставляла ее в очень сложном (как для пользователей, так и для разработчиков) виде.
Предоставив стандартный механизм КТТ1, мы сможем преодолеть барьер на пути использования библиотек из разных источников (см. раздел 8.2.2). Удастся реализовать механизм КТТ! логически последовательно, попытаемся сделать его безопасным, предупреждая о возможных неправильных способах использования.
Наконец, при проектировании С++ был заложен принцип: когда все сказано и сделано, надо доверять программисту. Возможная польза важнее, чем предполагаемые ошибки. Однако некоторые пользователи, в частности Джим Уолдо, энергично доказывают, что в КТТ1 возникает необходимость очень редко, а неправильное понимание, лежащее в основе всех ошибок применения данного средства, распространено широко, значит, общее воздействие этого механизма на С++ негативно. Только время покажет, кто прав.