Анимация
JavaScript
|
Главная Библионтека Counts operator=C const CountA ); Обычный virtual ~Count(); Обычный Итак, мы имеем класс, производные классы которого должны вызывать специальный конструктор Count, например, чтобы отслеживать количество объектов производных классов в системе. Это серьезная причина для использования виртуального наследования, которое позволит избежать двойного учета при .множественном наследовании, когда может случиться так, что у некоего производного класса окажется больше одного базового класса Count&. Интересно, заметили ли вы, что в классе count есть ошибка проектирования? У него есть неявно генерируемый копирующий конструктор, что, вероятно, является нежелательным для корректной работы счетчика. Для того чтобы исправить ситуацию, надо просто объявить закрытый копирующий конструктор без его определения: private: не определен; копирующего конструктора нет CountC const Count& ); Итак, мы хотим, чтобы класс count определял поведение дочерних производных классов. Но дети не всегда слушаются родителей, правда? ;-) К сожалению, программистам, как и всем остальны.м людям, свойственно ошибаться, так что иногда они будут забывать, что они обязаны явно написать две функции. class BadDerived : private virtual Count { int i ; конструктор no умолчанию: должен вызывать специальный конструктор, но делает ли он это? Вкратце - нет, конструктор по умолчанию не вызывает специализированный конструктор. Более того, существует ли вообще конструктор по умолчанию BadDerived? Ответ, который вряд ли покажется вам обнадеживающим, - отчасти. Имеется неявно объявленный конструктор по умолчанию (хорошо), но если вы попытаетесь его вы звать, у вас ничего не получится (плохо). Рассмотрим, почему так получается. Начнем с того, что BadDerived не определяет ни одного собственного конструктора, так что конструктор по умолчанию будет объявлен неявно. Но в тот момент, когда вы попытаетесь сго вызвать (напри.мер, при ссздании объекта BadDerived), этот конструктор по умолчанию становится неявно определенным или, как минимум, должен стать таковым. Однако поскольку неявно определенный конструктор, как предполагается, вызывает конструктор по умолчанию базового класса, который не существует, мы получаем неработоспособную программу. Отсюда можно заключить, что любая программа, которая попытается создать объект BadDerived, не соответствует правилам языка, и класс BadDerived совершенно справедливо назван некорректным. Так есть ли у данного класса конструктор по умолчанию? Отчасти. Он объявлен, ио при попытке вызвать сго выясняется, что он ни на что не годен. Если дети так себя ведут, такую семью трудно назвать счастливой. Копирующий конструктор: должен вызывать специальный конструктор, но делает ли он это? По те.м же причинам неявно сгенерированный копирующий конструктор будет объявлен, но при определении не будет вызывать специальный конструктор count. Как видно из исходного определения класса count, этот копирующий кон- Этот пример адаптирован из кода, приведенного в неопубликованной статье Марко Далла Гасперина (Marco Dalla Gasperina) "Подсчет объектов и виртуатьное наследование". Его код не имеет ошибок проектирования, о которых пойдет речь дальше. Тема этой статьи несколько отличается от рассматриваемой в данной задаче, но этот пример вполне применим для нее. структор будет просто вызывать неявно сгенерированный копирующий конструктор класса count. Если нам надо подавить неявно генерируемый копирующий конструктор count, как показано ранее, то класс BadDerived будет иметь неявно объявленный копирующий конструктор, но поскольку он не может быть неявно определен (т.к. копирующий конструктор Count оказывается недоступен), то все попытки его использования делают программу неработоспособной. К счастью, с этого момента начинаются хорошие новости. Копирующее присваивание: ок? деструктор: ок? Да, неявно сгенерированный оператор копирующего присваивания и деструктор будут работоспособны, а именно ~ будут вызывать (а в случае деструктора перекрывать) соответствующие функции базового класса. Так что хорошая новость в том, что хоть что-то работает правильно. Однако не все еще хорошо в .этой семье. В конце концов, в каждом домашнем хозяйстве должен быть минимальный порядок. Можем ли мы найти способ поддержания мира в этой семье? Не хочешь -- заставим! Имеется ли способ в контексте этого примера, при помощи которого автор класса Count мог бы заставить автора производного класса программировать в соответствии с указанными правилами - т.е. чтобы сообщение об ошибке вьщавалось во время компиляции (предпочтительно) или хотя бы во время выполнения программы, если автор производного класса нарушил указанное в комментарии к классу Count требование? Идея состоит не в том, чтобы запретить неявное объявление (мы не в состоянии это сделать), а в том, чтобы сделать неявное определение некорректным, чтобы ком-пилятор мог вразумительно сообщить о возникшей ошибке. Вопрос в общем виде: имеется ли способ, при помощи которого автор базового класса может заставить автора производного класса явным образом написать каждую из четырех перечисленных выше базовых операций? Если да, то как? Если нет, то почему? Все время, пока мы знакомились с неявным объявлением и определением четырех базовых операций, мы наталкивались на слова "недоступный" и "неоднозначный". Оказывается, что добавление неоднозначных перегрузок, даже с различными спецификаторами доступа, нам не сильно поможет. Трудно добиться чего-то лучшего, чем то, что мы получили, сделав функции базового класса выборочно недоступными, объявляя их закрытыми (определены ли они реально - вопрос второй) - и этот подход работает для всех функций, кроме одной. Пример 19-4: попытка заставить производный класс не использовать неявно сгенерированные функции, делая функции базового класса недоступными class Base { public: vi rtual -BaseO; private: BaseC const Base& ); he определена Base& operator=( const Base& ); не определена Этот класс Base не имеет конструктора по умолчанию (поскольку объявлен, хотя и НС определен, пользовательский конструктор), и имеет скрытые копирующий конструктор и копирующий оператор присваивания. Нет никакого способа скрыть деструктор. который должен всегда быть досгупен для производных классов и который обычно должен быть открытым и виртуальным, как в примере 19-4, или защищенным и невиртуальным (см. задачу 18). Идея заключается в том, что если мы даже захотим обеспечить поддержку данной операции (например, копирующего присваивания), то если мы не в состоянии сделать это при помощи обычной функции, то мы делаем эту обычную функцию недоступной и предоставляем другую функцию, которая делает необходимую нам работу. Что это нам дает? class Derived : private Base { int i ; конструктор no умолчанию: объявлен, но определение некорректно (нет конструктора Base по умолчанию) копирующий конструктор: объявлен, но определение некорректно (копирующий конструктор Base недоступен) копирующее присваивание: объявлено, но определение некорректно (копирующее присваивание Base недоступно) деструктор: все в порядке, будет компилироваться Не так плохо - мы получили три ошибки времени компиляции из четырех возможных, и оказывается, что это все, чего мы можем добиться. Это простое решение не в состоянии справиться с деструктором, но это не страшно, поскольку деструкторы в меньшей степени подвержены замене для специальных случаев. Базовый деструктор всегда должен быть вызван, тут двух мнений быть не может, кроме того, в конечном счете, может сущестюватъ только один деструктор. Трудность обычно представляет вызов необычного конструктора для корректной инициализации базового класса; после этого базовый класс может сохранить всю необходимую информацию для корректного выполнения деструктором всех стоящих перед ним задач. Итак, все оказалось не плохо -- впрочем, простые решения, как правило, наилучшие. В нашем случае, впрочем, есть несколько более сложных альтернатив. Давайте бегло с ними познакомимся, чтобы убедиться, что ни одна из них не в состоянии предложить более полное решение поставленной задачи. Альтернатива №1: сделать функции базового класса неоднозначными. Этот метод ничуть НС лучше: он так же не влияет на неявно сгенерированный деструктор и требует большего количества работы. Альтернатива №2: предоставить базовые версии функций, аварийно завершающие работу программы. Например, мы можем заставить их генерировать исключение std: :logic error. Это также не приводит к решению вопроса о неявно генерируемом деструкторе (не нарушает работу всех возможных деструкторов), а также превращает ошибку времени компиляции в ошибку времени выполнения, что существенно хуже. > Рекомендация Предпочитайте ошибки времени компиляции ошибкам времени выполнения. Альтернатива №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 |