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