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 typename T // типичная реализация std::swap
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 может быть объявлен следующим образом:
class WidgetImpl { // класс для данных Widget
public: // детали несущественны
...
private:
int a,b,c; // возможно, много данных –
std::vectordouble v; // копирование обойдется дорого
...
};
class Widget { // класс, использующий идиому pimpl
public:
Widget(const Widget rhs);
Widget operator=(const Widget rhs) // чтоб скопировать Widget, копируем
{ // его объект WidgetImpl. Детали
... // реализации operator= как такового
*pimpl = *(rhs.pimpl); // см. в правилах 10, 11 и 12
...
}
...
private: