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 одно из них компилируется, а другое – нет: