В C++, как и в других объектно-ориентированных языках программирования, при определении нового класса определяется новый тип. Потому большую часть времени вы как разработчик C++ будете тратить на совершенствование вашей системы типов. Это значит, что вы – не просто разработчик классов, но еще и разработчик типов. Перегруженные функции и операторы, управление распределением и освобождением памяти, определение инициализации и порядка уничтожения объектов – все это находится в ваших руках. Поэтому вы должны подходить к проектированию классов так, как разработчики языка подходят к проектированию встроенных типов.
Проектирование хороших классов – ответственная работа, и этим все сказано. Хорошие типы имеют естественный синтаксис, интуитивно воспринимаемую семантику и одну или более эффективных реализаций. В C++ плохо спланированное определение класса может сделать невозможным достижение любой из этих целей. Даже характеристики производительности функций-членов класса могут зависеть от того, как они объявлены.
Итак, как же проектировать эффективные классы? Прежде всего вы должны понимать, с чем имеете дело. Проектирование почти любого класса ставит перед разработчиком вопросы, ответы на которые часто ограничивают спектр возможных решений:
• Как должны создаваться и уничтожаться объекты нового типа? От ответа на этот вопрос зависит дизайн конструкторов и деструкторов, а равно функций распределения и освобождения памяти (оператор new, оператор new[], оператор delete и оператор delete[] – см. главу 8), если вы собираетесь их переопределить.
• Чем должна отличаться инициализация объекта от присваивания значений? Ответ на этот вопрос определяет разницу в поведении между конструкторами и операторами присваивания. Важно не путать инициализацию с присваиванием, потому что им соответствуют разные вызовы функций (см. правило 4).
• Что означает для объектов нового типа быть переданными по значению? Помните, что конструктор копирования определяет реализацию передачи по значению для данного типа.
• Каковы ограничения на допустимые значения вашего нового типа? Обычно только некоторые комбинации значений данных-членов класса являются правильными. Эти комбинации определяют инварианты, которые должен поддерживать класс. А инварианты уже диктуют, как следует контролировать ошибки в функциях-членах, в особенности в конструкторах, операторах присваивания и функциях установки значений («setter» functions). Могут быть также затронуты исключения, которые возбуждают ваши функции, и спецификации этих исключений.
• Укладывается ли ваш новый тип в граф наследования? Наследуя свои классы от других, вы должны следовать ограничениям, налагаемым базовыми классами. В частности, нужно учитывать, как объявлены в них функции-члены: виртуальными или нет (см. правила 34 и 36). Если вы хотите, чтобы вашему классу могли наследовать другие, то нужно тщательно продумать, какие функции объявить виртуальными; в особенности это относится к деструктору (см. правило 7).
• Какие варианты преобразования типов допустимы для вашего нового типа? Ваш тип существует в море других типов, поэтому должны ли быть предусмотрены варианты преобразования между вашим типом и другими? Если вы хотите разрешить
• Какие операторы и функции имеют смысл для нового типа? Ответ на этот вопрос определяет набор функций, которые вы объявляете в вашем классе. Некоторые из них будут функциями-членами, другие – нет (см. правила 23, 24 и 46).
• Какие стандартные функции должны стать недоступными? Их надо будет объявить закрытыми (см. правило 6).
• Кто должен получить доступ к членам вашего нового типа? Ответ на этот вопрос помогает определить, какие члены должны быть открытыми (public), какие – защищенными (protected) и какие – закрытыми (private). Также вам предстоит решить, какие классы и/или функции должны быть друзьями класса, а также когда имеет смысл вложить один класс внутрь другого.