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

Правило 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-конструкторе)

Оказывается, что к параметрам применимы неявные преобразования, только если они перечислены в списке параметров. Неявный параметр, соответствующий объекту, чья функция-член вызывается (тот, на который указывает this), никогда не подвергается неявному преобразованию. Вот почему первый вызов компилируется, а второй – нет. В первом случае параметр указан в списке параметров функции, а во втором – нет.

Однако вам хотелось бы получить полноценную поддержку смешанной арифметики, и теперь ясно, как ее обеспечить: нужен operator* в виде свободной функции, тогда компилятор сможет выполнить неявное преобразование всех аргументов:

class Rational {

... // не содержит operator*

};

const Rational operator*(const Rational& lhs, // теперь свободная функция

const Rational& rhs)

{

return Rational(lhs.numerator() * rhs.numerator(),

lhs.denominator() * rhs.denominator());

}

Rational oneFourth(1, 4);

Rational result;

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

result = 2 * oneFourth; // ура, работает!

Это можно было бы назвать счастливым концом, если бы не одно «но». Должен ли operator* быть другом класса Rational?

В данном случае ответом будет «нет», потому что operator* может быть реализован полностью в терминах открытого интерфейса Rational. Приведенный выше код показывает, как это можно сделать. И мы приходим к важному выводу: противоположностью функции-члена является свободная функция, а функция – друг класса. Многие программисты на C++ полагают, что раз функция имеет отношение к классу и не должна быть его членом (например, из-за необходимости преобразовывать типы всех аргументов), то она должна быть другом. Этот пример показывает, что такое предположение неправильно. Если вы можете избежать назначения функции другом класса, то должны так и поступить, потому что, как и в реальной жизни, друзья часто доставляют больше хлопот, чем хотелось бы. Конечно, иногда отношения дружественности оправданы, но факт остается фактом: если функция не должна быть членом, это не означает автоматически, что она должна быть другом.

Сказанное выше правда, и ничего, кроме правды, но это не вся правда. Когда вы переходите от «Объектно-ориентированного C++» к «C++ с шаблонами» (см. правило 1) и превращаете Rational из класса в шаблон класса, то вступают в силу новые факторы, новые способы их учета, и появляются неожиданные проектные решения. Все это является темой правила 46.

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

• Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса.

Данный текст является ознакомительным фрагментом.



Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

Что должно быть на главной странице

Из книги автора

Что должно быть на главной странице Главная страница любого интернет-магазина – с одной стороны, важная имиджевая составляющая, лицо, а с другой – не входная точка (место, куда посетитель попадает, впервые оказавшись на вашем сайте). Это далеко не всегда главная страница


Почему должно быть несколько способов для связи

Из книги автора

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


Преобразование типов

Из книги автора

Преобразование типов С++ представляет несколько синтаксических конструкций по приведению одного типа к другому. Заключение нужного типа результата в скобки и размещение его перед преобразуемым значением — это традиционный способ, унаследованный от С:const double Pi =


Преобразование типов данных

Из книги автора

Преобразование типов данных Типы данных существуют только для удобства программиста - VBA хранит всю свою информацию исключительно в цифровой форме. А поскольку это так, преобразование данных одних типов в другие не слишком большая проблема для VBA.В VBA есть целый ряд


44. Предпочитайте функции, которые не являются ни членами, ни друзьями

Из книги автора

44. Предпочитайте функции, которые не являются ни членами, ни друзьями РезюмеТам, где это возможно, предпочтительно делать функции не членами и не друзьями классов.ОбсуждениеФункции, не являющиеся членами или друзьями классов, повышают степень инкапсуляции путем


1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро

Из книги автора

1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро Программное обеспечение должно быть столь же прозрачным при выходе из строя, как и при нормальной работе. Лучше всего, если программа способна справиться


Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе

Из книги автора

Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе Существует много способов отслеживать время, поэтому имеет смысл создать базовый класс TimeKeeper и производные от него классы, которые реализуют разные подходы к хронометражу:class TimeKeeper


Правило 22: Объявляйте данные-члены закрытыми

Из книги автора

Правило 22: Объявляйте данные-члены закрытыми В этом правиле мы поговорим о том, почему данные-члены не должны быть открытыми (public). Затем мы убедимся, что все аргументы против открытых данных-членов касаются также защищенных (protected). Это приведет нас к выводу, что


Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса

Из книги автора

Правило 23: Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса Возьмем класс для представления Web-браузера. В числе прочих такой класс может предлагать функции, который очищают кэш загруженных элементов, очищают историю посещенных URL и


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

Из книги автора

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


1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро

Из книги автора

1.6.12. Правило исправности: когда программа завершается аварийно, это должно происходить явно и по возможности быстро Программное обеспечение должно быть столь же прозрачным при выходе из строя, как и при нормальной работе. Лучше всего, если программа способна справиться


Почему у программ не должно быть хозяев

Из книги автора

Почему у программ не должно быть хозяев Технология цифровой информации помогает человечеству, делая копирование и изменение информации проще. Компьютеры дают надежду, что это станет проще для каждого из нас.Hе все хотят, чтобы это стало проще. Система авторских прав дает