|
Программирование >> Обобщенные обратные вызовы
фейс это не оказывает никакого влияния: класс имеет все то же количество открытых функций для пользователей класса, так что им не надо ничего изучать дополнительно, и то же количество виртуальных функций, что и ранее, так что профаммисту производного класса тоже не прибавляется работы. Как видите, ни интерфейс для внешних пользователей, ни интерфейс наследования для производных классов не становятся сложнее, зато теперь они явным образом разделены, а это - хорошо. Итак, изложенный материал оправдывает невиртуальные интерфейсы и доказывает, что виртуальные функции только выифывают от закрытия. Но мы еше не ответили на вопрос, должны ли виртуальные функции быть закрытыми или защищенными. Ответ: > Рекомендация Лучше делать виртуальные функции закрытыми (private). Это просто. Такой подход позволяет производному классу переопределять функции для необходимой настройки поведения класса, при этом не делая их доступными для непосредственного вызова производным классам (что было бы возможно при объявлении функций защищенными). Дело в том, что виртуальные функции существуют для того, чтобы обеспечить возможность настройки поведения класса; если они при этом не должны непосредственно вызываться из кода производных классов, нет никакой необходимости в том, чтобы делать их не закрытыми. Однако иногда нам надо вызывать базовые версии виртуальных функций (см., например, [HyslopOO]), и только в этом случае имеет смысл делать эти виртуальные функции защищенными, т.е. мы можем сформулировать очередное правило. > Рекомендация Виртуальную функцию можно делать защищенной только в том случае, когда производному классу требуется вызов реализации виртуальной функции в базовом классе. Основной вывод заключается в том, что применение шаблона NVI к виртуальным функциям помогает нам отделить интерфейс от реализации. Можно добиться еще более полного разделения, если воспользоваться шаблоном типа Bridge [Gamma95], идиомой наподобие Pimpl (преимущественно для управления зависимостями во время компиляции и гарантий безопасности исключений) [SutterOO, Suttcrt)2] или более общими handle/body или envelope/letter [Coplien92], а также других подходов. Если же только вам не нужна большая степень разделения интерфейса и реализации, то NVI зачастую оказывается достаточно для ваших нужд. С другой стороны, применение NVI - хорошая идея, которую стоит принять по умолчанию в своей практической деятельности при создании нового кода и рассматривать как минимально необходимое разделение. В конце концов, она не приводит к дополнительным расходам (не считая написания дополнительной строки кода на функцию), но зато существенно уменьшает количество проблем впоследствии. Дополнительные примеры использования шаблона NVI для приватизации виртуального поведения можно найти в [HyslopOO]. Кстати о [HyslopOO] - вы не обратили внимания на то, что в представленном там коде имеется открытый виртуальный деструктор? Это приводит нас ко второй теме нашей задачи. Виртуальный вопрос №2: деструкторы базовых классов Теперь мы готовы заняться вторым классическим вопросом -- должны ли деструкторы базовых классов быть виртуальными? Как уже упоминалось, типичный ответ на такой вопрос: Конечно же, деструкторы базовых классов должны быть виртуальными! Этот ответ неправильный, и стандартная библиотека С++ содержит контрпримеры, опровергающие это мнение. Однако деструкторы базовых классов очень часто должны быть виртуальными, что и создает иллюзию корректности приведенного ответа. Немного менее распросфаненный и несколько более правильный ответ: Конечно, деструкторы базового класса должны быть виртуальными, если вы собираетесь удалять объекты полиморфно, т.е. через указатель на базовый класс. Этот ответ технически корректен, но не совсем полон. Я пришел к выводу, что полный корректный отеет должен звучать следующим образом. > Рекомендация Деструктор базового класса должен быть либо открытым и виртуальным, либо защищенным и невиртуальным. Давайте посмофим, почему. Понятно, что любая операция, которая выполняется посредством интерфейса базового класса и должна вести себя виртуально, должна быть виртуальна. Это справедливо даже при использовании NVI, поскольку хотя открытая функция и невиртуальна, она делегирует работу закрытой виртуальной функции, и таким образом мы получаем требуемое виртуальное поведение. Если уничтожение может быть выполнено полиморфно посредством интерфейса базового класса, то оно должно вести себя виртуально и, соответственно, быть виртуальным. В действительности, это требование языка - если вы выполняете полиморфное удаление без виртуального деструктора, то получаете весь спектр н е о п ре дс л с н н о го поведения , с которым лично я предпочел бы никогда не встречаться. Следовательно: пример 18-3: необходимость виртуального деструктора class Base { /* ... */ }; class Derived : public Base { /* ... */ }; Base- b = new Derived; delete b; Лучше бы Base::~Base быть виртуальным! Заметим, что деструктор - единственный случай, когда шаблон NVI не может быть применен к виртуальной функции. Почему? Потому что когда выполнение достигло тела деструктора базового класса, все части производных объектов уже уничтожены и более не существуют. Если в теле деструктора базового класса будет вызвана виртуальная функция, то выбор виртуальных функций не сможет пройти по иерархии классов дальше базового класса. В теле десфуктора (конструктора) порожденные классы уже (или еще) не существуют. Но базовые классы не всегда должны допускать полиморфное удаление. Рассмотрим, например, шаблоны классов, такие как std: :unary function и std: :binary function из стандартной библиотеки С++ [С++03]. Эти два шаблона классов выглядят следующим образом. template <class Arg, class Result> struct unary function { typedef Arg argument type; typedef Result result...type; template <class Argl, class Arg2, class Result> struct binary function { typedef Argl fi rst .argument type; typedef Arg2 second argument type; typedef Result result. type; Оба эти шаблона предназначены, в частности, для инстанцирования в качестве базовых классов (для ввода стандартных typedef-имен в производные классы) и не имеют виртуальных деструкторов, поскольку они не предназначены для полиморфного удаления. То есть, код наподобие следующего - не просто неразрешенный, но и просто незаконный. Поэтому вы можете с полным основанием считать, что такой код никогда не будет существовать. пример 18-4: проблематичный код, который никогда не будет существовать в реальности. void fС std::unary function* f ) { delete f; ошибка, не корректно Заметим, что стандарт не одобряет такие фокусы и объявляет пример 18-4 попадающим непосредственно в ловушку неопределенного поведения, если вы передадите указатель на объект, производный от std::unary function, но при этом не требует от компилятора запретить вам написать такой код (а жаль). Хотя это легко сделать - при этом ни в чем не нарушая стандарт - просто дать std::unary function (и другим классам наподобие него) пустой, но защищенный деструктор; в этом случае компилятор будет вынужден диагностировать ошибку и обвинить в ней нерадивого программиста. Может, мы увидим такое изменение в очередной версии стандарта, может - нет, но было бы неплохо, чтобы компилятор мог отвергнуть такой код. (Да, сделав деструктор защищенным, мы тем самым делаем невозможным непосредственное инстанцирование unary function, но это не имеет значения, поскольку этот шаблон полезен только в качестве базового класса.) А что если базовый класс конкретный (может быть создан объект данного класса), но вы хотите, чтобы он поддерживал полиморфное удаление? Должен ли его деструктор быть открытым - ведь иначе вы не сможете создать объект данного типа? Это возможно, но только если вы нарушили другое правило, а именно - ничего не порождайте из конкретных классов. Или, как сказал Скотт Мейерс (Scott Meyers) в разделе [Meyers96], делайте классы-нс листья абстрактными . (Конечно, на практике можно столкнуться с нарушением этого правила - понятно, в чьем-то коде, не в вашем - ив этом единственном случае вам придется иметь открытый виртуальный деструктор просто для того, чтобы приспособиться к уже имеющемуся скверному дизайну. Конечно, лучше - если это возможно - изменить сам дизайн.) Коротко говоря, вы оказываетесь в одной из двух ситуаций. Либо а) вы хотите обеспечить возможность полиморфного удаления через указатель на базовый класс, и тогда деструктор должен быть открытым и виртуальным, либо б) вам этого не нужно, и тогда деструктор должен быть невиртуальным и защищенным - чтобы предотвратить нежелательное использование вашего класса. Резюме Итак, лучше делать виртуальные функции базового класса закрытыми (или, при наличии веских оснований, защищенными). Это разделяет интерфейс и реализацию, что позволяет стабилизировать интерфейс и упростить дальнейшие изменения и переработки реализации. Для функций обычного базового класса: рекомендация №1: лучше делать интерфейс невиртуальным, с использованием шаблона проектирования невиртуального интерфейса (NV1); рекомендация №2: лучше делать виртуальные функции закрытыми (private); 29 Иначе говоря, классы, соответствующие внутренним узлам дерева наследования. - Прим. перев. 124 Разработка классов, наследование и полиморфизм
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |