|
Программирование >> Полиморфизм без виртуальных функций в с++
Относительная легкость, с которой можно имитировать мультиметоды в любом конкретном примере, и была той причиной, по которой решение данной проблемы достаточно долго не представлялось срочным. Как ни суди, это не что иное, как техника, всегда применявшаяся для имитации виртуапьных функций в С. Такие обходные пути приемлемы, если нужда в них возникает ие слишком часто. 13.9. Защищенные члены Простая модель сокрытия данных на основе закрытых и открытых секций класса прекрасно работала в C+-I-, пока он использовался в основно.м как язык для абстрагирования данных. Данная модель была вполне пригодна и для большого класса задач, где наследование при.менялось для объектно-ориентированного програм.мирования. Однако, когда речь заходит о классах, выясняется, что к любому из них обращаются либо производные от пего классы, либо прочие классы и функции. Функции-члены и дружественные функции, реализуюппе операции с классом, воздействуют на его объекты от имени одного из этих пользователей . Механизм открытых и закрытых членов позволяет четко различать автора класса и т.п., но не дает возможности приспособить поведение к нужда.м производных классов. Вскоре после выхода версии 1.0 ко .мне в кабинет .заглянул Марк Линтон и умолял добавить третий уровень контроля доступа, который напрямую поддержал бы стиль, применяемый в библиотеке Interviews (см. раздел 8.4.1). Она разрабатывалась в Стэнфорде. Мы выбрали слово protected для обозначешш членов класса, которые вели себя как открытые для прочих членов самого этого класса и производных от него, но как закрытые для всех остальных пользователей . Марк был главны.м создателем библиотеки Interviews. Основываясь на опыте и примерах из настоящих програ.\гм, он убедительно доказывы, что защищенные данные абсолютно необходимы при проектировании эффективной и расширяемой инструментальной библиотеки для X Windows. Альтернативой защищенным данным, по его словам, были неприемлемо низкая эффективность, неуправляемое распространение встраиваемых интерфейсных функций или глобальные данные. Защищенные данные и, в более широком смысле, защищенные члены казались меньщим злом. Кроме того, так называемые чистые языки вроде Smalltalk поддерживали именно такую - довольно слабую - защиту вместо более сильной защиты С++, основанной на ко1щепции private. Мне самому приходилось писать код, в котором данные были объявлены как public просто для того, чтобы и.ми можно было пользоваться в производных классах. Видел я также и програм.мы, в которых концепция friend использовалась неудачно и лишь ради того, чтобы дать доступ явно поименованным производным класса.м. Это были веские аргументы, убедившие .меня в том, что защищенные члены надо разрешить. Однако веские аргу.менты могут найтись для каждого .мыслимого языкового средства и для любого воз.можного его использования. На.м же нужны факты. Не имея фактов и должным образом осознанного опыта, мы уподобляемся греческим философам, которые на протяжении неско.чъких веков вели блистательные дискуссии, но так и не смогли договориться, из каких же четырех (а может, пяти) основных субстанций состоит Вселенная. Примерно через пять лет Марк запретил использование защищенных членов-да1И1ых в Interviews, поскольку они стали источником ошибок и серьезно усложнили сопровождение. В пленарном докладе Барбары Лисков (Barbara Liskov) на конференции OOPSLA [Liskov, 1987J достаточно подробно описываются теоретические и практические проблемы, связанные с контролем доступа на основе концепции protected. Moii опыт показывает, что всегда существовали альтернативы размещению инфор.мации в общем базовом классе, где она была бы доступ-ной производным классам. Я сомневался по поводу protected еще и потому, что при небрежном программировании обши11 базовьш класс слишко.м уж легко использовать как разновидность глобальных данных. К счастью, вас никто не принуждает пользоваться защищенными данными в C-I-+. По умолчанию члены класса и.меют атрибут private, и обычно такой выбор лучше. Замечу, впрочем, что ни одно из этих возражений не относится к защи-nicHHbiM членам-функциям. Убежден, 4Toprotected прекрасно подходит для специфицирования операций, которы.ми можно пользоваться в производных классах. Защищенные члены появились в версии 1.2. Защищенные базовые классы были впервые описаны в ARM и включены в версию 2.1. Оглядываясь назад, я думаю, что protected - это тот случай, когда веские аргументы и мода сбили меня с пути истинного и заставили отказаться от своих убеждений ради новых воз.можностей. 13.10. Улучшенная генерация кода Для некоторых пользователей са.мой важной особенностью версии 2.0 была не какая-то новая воз.можпость, а простая оптимизация использования памяти. С са.мого начала Cfront генерировал неплохой код. Даже в 1992 г. код, произведенный Cfront, был лучшим в сравнительных тестах компиляторов С++ на плат-фор.ме SPARC. Если не считать оптимизации возврата значений, предложенной в [ARM, §12.1с] и реализованной в версии 3.0, то никаких существенных улучшений в области генерации кода с версии 1.0 в Cfront не было. Но первая версия впустую расходовала память, так как для каждой единицы трансляции генерировался собственный набор таблиц виртуальных функций всех классов, использовавшихся в этой единице. Объем напрасно потраченной памяти мог достигать нескольких мегабайтов. В то время (примерно 1984 г.) я считал этот расход необходимым из-за отсутствия поддержки со стороны компоновщика. К 1987 г. поддержки так и пе появилось. Поэто.му я вернулся к проблеме и решил ее с помощью простого приема, разместив таблицу виртуальных функций класса сразу после определения первой невиртуальной и невстраиваемой функции в нем. Напри.мер: class X { public: virtual void f1() { /* ... */ } void f2{); virtual void f3() = 0; virtual void i4(); первая невстраиваемая невиртуальная функция . . . в каком-то файле void X::f4 О { /* ... */ } Cfront поместит таблицу виртуальных функций здесь Я выбрал такой вариант, поскольку он осуществляется вне зависимости от компоновщика. Рещение несовершенно, так как память все равно тратится зря для классов, в которых нет невстраиваемых невиртуальных функций, но подобные расходы перестали быть серьезной нробле-мой на практике. В обсуждении деталей такой оптимизации принимали участие Эндрю Кениг и Стэн Липпман. Конечно, в других компиляторах может быть реализовано иное решение, более соответствующее среде, в которой они работают. В качестве альтернативы мы рассматривали такую воз.можность: оставить генерацию таблиц виртуальных функций в каждой единице трансляции, а затем запустить препроцессор, который удалит все таблицы, кроме одной. Однако это трудно было сделать переносимым способом, кроме того, способ неэффективен. Зачем генерировать таблицы, а потом только тратить время на их удаление? Но вазможно-сти остаются открыты.ми для тех, кто готов создавать собственный компоновщик. 13.11. Указатели на функции-члены Первоначально в языке не было способа представить указатель на функцию-член. Приходилось использовать обходные пути и преодолевать офаничение в таких случаях, как обработка ошибок, где традиционно использовались указатели на функции. Если есть объявление struct S { int mf(char*); to no привычке писался такой код: typedef void {*PSmem)(S*,char*); PSmem m = (PSmem)uS::mf; void g(S* ps) { m(ps, hello ) ; Для того чтобы данный прием работал, приходилось повсюду включать в код явные приведения типов, которых в принципе не должно было быть. Этот код к тому же базировался на предположении, что функции-члену передается указатель на объект ее класса ( указатель this ) в качестве первого аргумента, то есть так, как реализовано в Cfront (см. раздел 2.5.2). Еще в 1983 г. я пришел к выводу, что такой вариант никуда не годится, но считал, что данную проблему легко решить и те.м са.мым закрыть дыру в системе
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |