Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 76
Текст из файла (страница 76)
Оставшиеся две функции в классе ЯЬаре — просто оптимизация, возможная, если о производном классе все известно во время проектирования базового. Этот прием получил название двойной диспетчеризации и впервые был описан в работе ~1пйа11я, 19861. Для С++ данный метод плох тем, что при добавлении класса в иерархию необходимо изменять уже существующие классы.
Производный класс, например нессапд1е, должен «знать» обо всех своих «братьях», иначе не удастся составить правильный набор виртуальных функций. Так, при добавлении класса Тг1апд1е придется модифицировать и Нессапд1е, и С1гс1е, а также — если желательна показанная выше оптимизация — и ЯЬаре. с1аяя аесгапд1е : рпЬ11с яьаре ( // ьоо1 1пгегяесг(сопяс БЬареь); ьоо1 1псегяесг(сопел кесгапд1еа); Ьоо1 1пгегяесг(сопел С1гс1еь); ьоо1 1псегяесг(сопяс тгтапд1еа); ); В целом двойная диспетчеризация в С++ — это эффективный метод «иерархического продвижения», если есть возможность модифицировать объявления при добавлении новых классов и если набор производных классов меняется не слишком часто.
Иной способ — хранить в объектах определенный идентификатор типа и после его проверки выбирать, какую функцию вызывать. Использование функции суре16 ( ) для идентификации типа во время выполнения (см. раздел 14.25)— одна из возможностей. Можно завести структуру, где хранятся указатели на функции, и использовать для доступа к ней идентификатор типа.
Достоинство данного метода в том, что базовый класс ничего не должен «знать» о сушествовании производных. Например, при наличии подходяших определений функция Ьоо1 1псегяесс(попас Япаре* я1, сопел БЬаре* я2) ( 1пс г = г1по 1пс)ех(я1.суре 1о(),я2.гуре мз()); 11 (1 < О) еггог("плохой индекс"); ехгегп Рос саЬ1е* гЬ1; Рог 1 = гЬ1(г]; гегпгп 1(я1,я2); ) будет вызывать правильную функцию при любых допустимых типах обоих своих аргументов. По сути дела, это реализация вручную вышеупомянутой таблицы виртуальных функций для мультиметодов. ':6ИИИИИИИ Защищенные члены Относительная легкость, с которой можно имитировать мультимстоды в любом конкретном примере, и была той причиной, по которой решение данной проблемы достаточно долго не представлялось срочным, Как ни суди, это нс что иное, как техника, всегда применявшаяся для имитации виртуальных функций в С.
Такие обходные пути приемлемы, если нужда в них возникает не слишком часто. 13.9. Защищенные члены Простая модель сокрьпия данных на основе закрытых н открытых секций класса прекрасно работала в С++, пока он использовался в основном как язык для абстрагирования данных. Данная модель была вполне пригодна н для большого класса задач, где наследование применялось для объектно-ориентированного программирования. Однако, когда речь заходит о классах, выясняется, что к любому из них обращаются либо производные от него классы, либо прочие классы и функции. Функции-члены и дружественные функции, реализующие операции с классом, воздействуют на его объекты от имени одного из этих «пользоватслсйм Механизм открытых и закрытых членов позволяет четко различать автора класса и т.п., но не дает возможности приспособить поведенис к нуждам производных классов.
Вскоре после выхода версии 1.0 ко мне в кабинет заглянул Марк Липтон и умолял добавить третий уровень контроля доступа, который напрямую поллержал бы стиль, применяемый в библиотеке 1псег»1сшз (см. раздел 8.4.1). Она разрабатывалась в Стзнфорде. Мы выбрали слово ргос ес" ес1 для обозначения членов класса, которые вели себя как «открытые» для прочих членов самого этого класса и производных от него, но как «закрытые» для всех остальных «пользователей». Марк был главным создателем библиотеки 1псеглеж, Основываясь на опыте и примерах из настоящих программ, он убедительно доказывал, что защищенные данные абсолютно необходимы при проектировании эффективной и расширяемой инструментальной библиотеки для Х '»1чпг(о»чэ.
Альтернативой защищенным данным, по его словам, были неприемлемо низкая эффективность, неуправляемое распространение встраиваемых интерфейсных функций или глобальные данные. Защищенные данные и, в более широком смысле, защищенныс члены казались меньшим злом. Кроме того, так называемые чистые языки вроде БшаИга(к поддерживали именно такую — довольно слабую — защиту вместо более сильной зашиты С++, основанной на концепции рг тра се. Мне самому прихолилось писать код, в котором данные были объявлены как риЬ11с просто для того, чтобы ими можно было пользоваться в производных классах.
Видел я также и программьь в которых концепция йгйепс) использовалась неулачно и лишь ради того, чтобы дать доступ явно поименованным производным классам. Это были веские аргументы, убедившие меня в том, что зашигценные члены надо разрешить. Однако «веские аргументы» могут найтись для каждого мыслимого языкового средства и для любого возможного его использования. Нам же нужны факты. Не имея фактов и должным образом осознанного опыта, мы уподобляемся греческим философам, которые на протяжении нескольких веков вели блистательные дискуссии, но так и не смогли договориться, из каких же четырех (а может, пяти) основных субстанций состоит Вселенная. ППЗИИИИВ Уточнения понятия класса 13.10.
Улучшенная генерация кода Для некоторых пользователей самой важной особенностью версии 2.0 была нс какая-то новая возможность, а простая оптимизация использования памяти. С самого начала С1гопг генерировал неплохой код. Даже в 1992 г. код, произведенный С1гопс, был лучшим в сравнительных тестах компиляторов С++ на платформе БРАКС. Если не считать оптимизации возврата значений, предложенной в [АКМ, 912.1с] и реализованной в версии 3.0, то никаких сушественных улучшений в области генерации кода с версии 1.0 в С1гопг не было.
Но первая версия впустую расходовала память, так как для каждой единицы трансляции генерировался собственный набор таблиц виртуальных функций всех классов, использовавшихся в этой единице. Объем напрасно потраченной памяти мог достигать нескольких мегабайтов. В то время О)риыерно 1984 г.) я считал этот расход необходимым из-за отсутствия поддержки со стороны компоновщика. К 1987 г. поддержки так и не появилось. Поэтому я вернулся к проблеме и решил ее с помощью простого приема, разместив таблицу виртуальных функций класса сразу после определения первой невиртуальной и невстраиваемой функции в нем.
Например: с12яя Х рцЬ11с: ч1тлца1 той 11() ( у* ... *! ) уоЫ 12(); у1теца) уоьо 13() = О; чггоиа1 той 4(); уу первая невстраиваемая уу нелиртуальная функция I! Примерно через пять лет Марк запретил использование защищенных членов- данных в 1пгегу(е)яз, поскольку они стали источником ошибок и серьезно усложнили сопровождение. В пленарном докладе Барбары Дисков 1ВагЪага 1лзйоу) на конференции ООРБЕА (1(зйок 1987~ достаточно подробно описываются теоретические и практические проблемы, связанные с контролем доступа на основе концепции ргогессеб. Мой опыт показывает, что всегда суп(ествовали альтернативы размещению информации в обшем базовом классе, где она была бы доступной производным классам.
Я сомневался по поводу ргосесс ес) еше и потому, что при небрежном программировании обший базовый класс слишком уж легко использовать как разновидность глобальных данных. К счастью, вас никто не принуждает пользоваться защищенными данными в С++. По умолчанию члены класса имеют атрибут ргфчасе, и обычно такой выбор лучше. Замечу, впрочем, что ни одно из этих возражений не относится к защищенным членам-функциям. Убежден, что ргосессе(1 прекрасно подходит для специфицирования операций, которыми можно пользоваться в производных классах.
Зашншепные члены появились в версии 1.2. Защншенные базовые классы были впервые описаны в АКМ и включены в версию 2.1. Оглядываясь назад, я думаю, что ргосес пес) — это тот случай, когда «веские аргументыь и мода сбили меня с пути истинного и заставили отказаться от своих убеждений ради новых возможностей. ИИИИИИКИ Указатели на функции-члены !! в каком-то файле вз х::ы )) ) у* ...
*! ) !! Сбгопг поместит таелицу виртуальных Функций здесь 13.11. Указатели на функции-члены Первоначалыю в языке не было способа представить указатель иа функциючлеп. Приходилось использовать обходные пути и преодолевать ограничение в таких случаях, как обработка ошибок, где традиционно использовались указатели па функции. Если есть объявление вггпсс Б ( )пс шг (с)1аг*); ) то по привычке писался такой код: сурес)еГ уоьа (*РБшеш) )Б*, слал*) ) РБшеш ш = )РБшеш)аБ::шб; ло)Д д)Б* рв) ( п)рв, "'пе'уо"); ) Для того чтобы данный прием работал, приходилось повсюду включать в код явные приведения типов, которых в принципе не должно было быть.
Этот код к тому же базировался на предположении, ио функции-члену передается указатель па объект ес класса («указатель с)пк Ба) в качестве первого аргумента, то есть так, как реализовано в СГгопг (см, раздел 2.5.2). Еще в ) 983 г. я пришел к выводу, что такой вариант никуда не годится, но считал, что ланную проблему легко решить и тем самым закрыть «дыру» в системе Я выбрал такой вариант, поскольку он осуществляется вне зависимости от компоновщика.
решение несовершенно, так как память все равно тратится зря для классов, в которых нет невстраиваемых невиртуальных функций, но подобные расходы перестали быть серьезной проблемой на практике. В обсуждении деталей такой оптимизации принимали участие Эндрю Кениг и Стэн Липпман. Конечно, в других компиляторах может быть реализовано иное решение, более соответствующее среде, в которой оин работают. В качестве альтернативы мы рассматривали такую возможность: оставить генерацию таблиц виртуальных функций в каждой единице трансляции, а затем запуст ить препропессор, который удалит все таблицы, кроме одной.