– означает также, что пользователи могут легко расширять набор вспомогательных функций. Для этого нужно лишь поместить новые функции (нечлены и недрузья) в то же пространство имен. Например, если пользователь класса WebBrowser решит дописать вспомогательные функции, имеющие отношение к загрузке изображений, он должен будет создать заголовочный файл, включающий объявления этих функций в пространство имен Web-BrowserStuff. Новые функции становятся после этого так же доступны, как и все прочие вспомогательные функции. Это еще одно свойство, которое не могут представить классы, потому что определения классов закрыты для расширения клиентами. Конечно, клиенты могут создавать производные классы, но они не будут иметь доступа к инкапсулированным (то есть закрытым) членам базового класса, поэтому таким образом «расширенную» функциональность уже не назовешь первоклассной. Кроме того, в правиле 7 объясняется, что не все классы предназначены для того, чтобы быть базовыми.

Что следует помнить

• Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса. Это повышает степень инкапсуляции и расширяемости, а также гибкость «упаковки» функциональности.

Правило 24: Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам

Во введении я отмечал, что в общем случае поддержка классом неявных преобразований типов – неудачная мысль. Но, конечно, из этого правила есть исключения, и одно из наиболее важных касается создания числовых типов. Например, если вы проектируете класс для представления рациональных чисел, то неявное преобразование целого числа в рациональное выглядит вполне разумно. Уж во всяком случае не менее разумно, чем встроенное в C++ преобразование int в double (и куда разумнее встроенного преобразования из double в int). Коли так, то начать объявления класса Rational можно было бы следующим образом:

class Rational {

public:

Rational(int numerator = 0,

int denominator = 1); // конструктор сознательно не explicit;

// допускает неявное преобразование

// int в Rational

int numerator() const; // функции доступа к числителю и

int denominator() const; // знаменателю – см. правило 22

private:

...

};

Вы знаете, что понадобится поддерживать арифметические операции (сложение, умножение и т. п.), но не уверены, следует реализовывать их посредством функций-членов или свободных функций, возможно, являющихся друзьями класса. Инстинкт говорит: «Сомневаешься – придерживайся объектно-ориентированного подхода». Вы понимаете, что, скажем, умножение рациональных чисел относится к классу Rational, поэтому кажется естественным реализовать operator* в самом этом классе. Но наперекор интуиции правило 23 утверждает, что идея помещения функции внутрь класса, с которым она ассоциирована, иногда противоречит объектно-ориентированным принципам. Впрочем, оставим на время эту тему и посмотрим, во что выливается объявление operator* функцией-членом Rational:

class Rational {

public:

...

const Rational operator*(const Rational& rhs) const;

}

Если вы не понимаете, почему эта функция объявлена именно таким образом (возвращает константный результат по значению и принимает ссылку на const в качестве аргумента), обратитесь к правилам 3, 20 и 21.

Такое решение позволяет легко манипулировать рациональными числами:

Rational oneEighth(1, 8);

Rational one Half(1, 2);

Rational result = oneHalf * oneEighth; // правильно

result = result * oneEighth; // правильно

Но вы не удовлетворены. Хотелось бы поддерживать также смешанные операции, чтобы Rational можно было умножить, например, на int. В конце концов, это довольно естественно – иметь возможность перемножать два числа, даже если они принадлежат к разным числовым типам.

Однако если вы попытаетесь выполнить смешанные арифметические операции, то обнаружите, что они работают только в половине случаев:

result = oneHalf * 2; // правильно

result = 2 * oneHalf; // ошибка!

Это плохой знак. Умножение должно быть коммутативным (не зависеть от порядка сомножителей), помните?

Источник проблемы становится понятным, если переписать два последних выражения в функциональной форме:

result = oneHalf.operator*(2); // правильно

result = 2.operator*(oneHalf); // ошибка!

Объект oneHalf – это экземпляр класса, включающего в себя operator*, поэтому компилятор вызывает эту функцию. Но с целым числом 2 не ассоциирован никакой класс, а значит, нет для него и функции operator*. Компилятор будет также искать функции operator*, не являющиеся членами класса (в текущем пространстве имен или в глобальной области видимости):

result = operator*(2, oneHalf); // ошибка!

Но в данном случае нет и свободной функции operator*, которая принимала бы аргументы int и Rational, поэтому поиск завершится ничем.

Посмотрим еще раз на успешный вызов. Видите, что второй параметр – целое число 2, хотя Rational::operator* принимает в качестве аргумента объект Rational. Что происходит? Почему 2 работает в одной позиции и не работает в другой?

Происходит неявное преобразование типа. Компилятор знает, что вы передали int, а функция требует Rational, но он также знает, что можно получить подходящий объект, если вызвать конструктор Rational c переданным вами аргументом int. Так он и поступает. Иными словами, компилятор трактует показанный выше вызов, как если бы он был написан примерно так:

const Rational temp(2); // создать временный объект Rational из 2

result = oneHalf * temp; // то же, что oneHalf.operator*(temp);

Конечно, компилятор делает это только потому, что есть конструктор, объявленный без квалификатора explicit. Если бы квалификатор explicit присутствовал, то ни одно из следующих предложений не скомпилировалось бы:

result = oneHalf * 2; // ошибка! (при наличии explicit-конструктора):

// невозможно преобразовать 2 в Ratinal

result = 2 * oneHalf; // та же ошибка, та же проблема

Со смешанной арифметикой при таком подходе придется распроститься, но, по крайней мере, такое поведение непротиворечиво.

Ваша цель, однако, – обеспечить и согласованность, и поддержку смешанной арифметики, то есть нужно найти такое решение, при котором оба предложения компилируются. Это возвращает нас к вопросу о том, почему даже при наличии explicit-конструктора в классе Rational одно из них компилируется, а другое – нет:

result = oneHalf * 2; // правильно (при не explicit-конструкторе)

result = 2 * oneHalf; // ошибка! (даже при не explicit-конструкторе)

Оказывается, что к параметрам применимы неявные преобразования, только

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ОБРАНЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату