![]() |
![]() |
![]() |
Анимация
JavaScript
|
Главная Библионтека Как уже упоминалось, типичный ответ на такой вопрос: "Конечно же, деструкторы базовых классов должны быть виртуальными!" Этот ответ неправильный, и стандартная библиотека С++ содержит контрпримеры, опровергающие это мнение. Однако деструкторы базовых классов очень часто должны быть виртуальными, что и создает иллюзию корректности приведенного ответа. Немного менее распросфаненный и несколько более правильный ответ: "Конечно, деструкторы базового класса должны быть виртуальными, если вы собираетесь удалять объекты полиморфно, т.е. через указатель на базовый класс." Этот ответ технически корректен, но не совсем полон. Я пришел к выводу, что полный корректный ответ должен звучать следующим образом. > Рекомендация Десфуктор базового класса должен быть либо открытым и виртуальным, либо защищенным и невиртуальным. Давайте посмотрим, почему. Понятно, что любая операция, которая выполняется посредством интерфейса базового класса и должна вести себя виртуально, должна быть виртуальна. Эго справедливо даже при использовании 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 Разработка классов, наследование и полиморфизм • рекомендация №3: виртуальную функцию можно делать защищенной только в том случае, когда производному классу требуется вызов реализации виртуальной функции в базовом классе. Единственное исключение делается для деструктора: • рекомендация №4: деструктор базового класса должен быть либо открытым и виртуальным, либо защищенным и невиртуальным. Правда, сама стандартная библиотека не всегда следует всем этим рекомендациям. Частично это отражение процесса накопления знаний сообществом программистов С++. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 [ 36 ] 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
![]() |