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

We use cookies. Read the Privacy and Cookie Policy

Правило 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), то функция не должна быть членом класса.

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