Оказывается, что к параметрам применимы неявные преобразования,
Однако вам хотелось бы получить полноценную поддержку смешанной арифметики, и теперь ясно, как ее обеспечить: нужен 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 из класса в
• Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса.
Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений
swap – интересная функция. Изначально она появилась в библиотеке STL и с тех пор стала, во-первых, основой для написания программ, безопасных в смысле исключений (см. правило 29), а во-вторых, общим механизмом решения задачи и присваивания самому себе (см. правило 11). Раз уж swap настолько полезна, то важно реализовать ее правильно, но рука об руку с особой важностью идут и особые сложности. В этом правиле мы исследуем, что они собой представляют и как с ними бороться.
Чтобы обменять (swap) значения двух объектов, нужно присвоить каждому из них значение другого. По умолчанию такой обмен осуществляет стандартный алгоритм swap. Его типичная реализация не расходится с вашими ожиданиями:
namespace std {
template
void swap(T& a, T& b) // меняет местами значения a и b
{
T temp(a);
a = b;
b = temp;
}
}
Коль скоро тип поддерживает копирование (с помощью конструктора копирования и оператора присваивания), реализация swap по умолчанию позволяет объектам этого типа обмениваться значениями без всяких дополнительных усилий с вашей стороны.
Стандартная реализация swap, может быть, не приведет вас в восторг. Она включает копирование трех объектов: a в temp, b в a и temp – в b. Для некоторых типов ни одна из этих операция в действительности не является необходимой. Для таких типов swap по умолчанию – быстрый путь на медленную дорожку.
Среди таких типов сразу стоит выделить те, что состоят в основном из указателей на другой тип, содержащий реальные данные. Общее название для таких проектных решений: «идиома pimpl» (pointer to implementation – указатель на реализацию – см. правило 31). Спроектированный так класс Widget может быть объявлен следующим образом: