Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Во-вторых, из того, что забота об инкапсуляции требует, чтобы функция не была членом класса, вовсе не следует, что эта функция не может быть членом какого-то другого класса. Это может облегчить жизнь программистам, привыкшим к языкам, в которых все функции должны быть членами классов (например, Eiffel, Java, C# и т. п.). Например, мы можем сделать clearBrowser статической функцией-членом некоторого служебного класса. До тех пор пока она не является частью (или другом) класса WebBrowser, она никак не скажется на инкапсуляции его закрытых членов.
В C++ более естественно объявить clearBrowser свободной функцией в том же пространстве имен, что и класс WebBrowser:
Но
Для класса, подобного WebBrowser, можно было бы определить много таких вспомогательных функций: для работы с закладками, вывода на печать, управления «куками» и т. п. Вообще говоря, большинству пользователей будут интересны только некоторые из этих функций. Но с какой стати компиляция пользовательской программы, в которой используются только функции, относящиеся к закладкам, должна зависеть, например, от наличия функций управления «куками»? Самый простой способ разделить их – это объявить функции, относящиеся к закладкам, в одном заголовочном файле, функции управления «куками» – в другом, функции поддержки печати – в третьем и так далее:
Отметим, что именно так организована стандартная библиотека C++. Вместо единственного монолитного заголовка <С++ StandardLibrary>, содержащего все, что есть в пространстве имен std, существуют десятки более мелких заголовочных файлов (например, <vector>, <algorithm>, <memory> и т. п.). В каждом из них объявлена некоторая функциональность из std. Пользователь, которому нужно только то, что имеет отношение к векторам, может не включать в свою программу директиву #include <memory>, а пользователь, не нуждающийся в списках, не обязан включать #include <list>. Поэтому на этапе компиляции пользовательские программы зависят только от тех частей системы, которые они действительно используют (см. в правиле 31 обсуждение других способов уменьшения зависимостей компиляции). Подобное разделение функциональности невозможно, если она обеспечивается функциями-членами класса, потому что класс должен быть определен полностью, его нельзя разбить на части.
Размещение вспомогательных функций в разных заголовочных файлах, но в одном пространстве имен – означает также,
• Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса. Это повышает степень инкапсуляции и расширяемости, а также гибкость «упаковки» функциональности.
Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам
Во введении я отмечал, что в общем случае поддержка классом неявных преобразований типов – неудачная мысль. Но, конечно, из этого правила есть исключения, и одно из наиболее важных касается создания числовых типов. Например, если вы проектируете класс для представления рациональных чисел, то неявное преобразование целого числа в рациональное выглядит вполне разумно. Уж во всяком случае не менее разумно, чем встроенное в C++ преобразование int в double (и куда разумнее встроенного преобразования из double в int). Коли так, то начать объявления класса Rational можно было бы следующим образом:
Вы знаете, что понадобится поддерживать арифметические операции (сложение, умножение и т. п.), но не уверены, следует реализовывать их посредством функций-членов или свободных функций, возможно, являющихся друзьями класса. Инстинкт говорит: «Сомневаешься – придерживайся объектно-ориентированного подхода». Вы понимаете, что, скажем, умножение рациональных чисел относится к классу Rational, поэтому кажется естественным реализовать operator* в самом этом классе. Но наперекор интуиции правило 23 утверждает, что идея помещения функции внутрь класса, с которым она ассоциирована, иногда противоречит объектно-ориентированным принципам. Впрочем, оставим на время эту тему и посмотрим, во что выливается объявление operator* функцией-членом Rational:
Если вы не понимаете, почему эта функция объявлена именно таким образом (возвращает константный результат по значению и принимает ссылку на const в качестве аргумента), обратитесь к правилам 3, 20 и 21.
Такое решение позволяет легко манипулировать рациональными числами:
<