Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект
Как только программисты осознают проблемы эффективности, связанные с передачей объектов по значению (см. правило 20), они, подобно крестоносцам, преисполняются решимости искоренить зло – передачу по значению – везде, где бы оно ни пряталось. Непреклонные в своем «святом» порыве, они с неизбежностью допускают фатальную ошибку: начинают передавать по ссылке значения несуществующих объектов. А это неправильно.
Рассмотрим класс для представления рациональных чисел, включающий в себя дружественную функцию для перемножения двух таких чисел:
class Rational {
public:
Rational(int numerator = 0, // см. в правиле 24 – почему этот
int denominator = 1); // конструктор не explicit
...
private:
int n, d;
friend
const Rational // см. в правиле 3 -
operator*(const Rational& lhs, // почему возвращаемый тип const
const Rational& rhs);
};
Ясно, что эта версия operator* возвращает результирующий объект по значению, и вы обнаружили бы непрофессиональный подход, если бы не уделили внимания вопросу о затратах на создание и удаление объекта. Вы не хотите платить за то, за что платить не должны. Отсюда вопрос: должны ли вы платить?
Нет, если можете вернуть ссылку. Но ссылка – это просто другое
Очевидно, нет никаких оснований полагать, что такой объект существует до вызова operator*. Например, если у вас есть
Rational a(1, 2); // a = 1/2
Rational a(3, 5); // b = 3/5
Rational c = a*b; // c должно равняться 3/10
то неразумно ожидать, что уже существует то рациональное число со значением три десятых. Если operator* будет возвращать такое число, то он должен создать его самостоятельно.
Функция может создать новый объект только двумя способами: в стеке или в куче. Создание в стеке осуществляется посредством определения локальной переменной. Используя эту стратегию, вы можете попытаться написать operator* так:
const Rational& operator*(const Rational& lhs, // предупреждение!
const Rational& rhs) // плохой код!
{
Rational result(lhs.n * rhs.h, lhs.d * rhs.d);
return result;
}
Этот подход можно отвергнуть сразу, потому что вашей целью было избежать вызова конструктора, а result должен быть создан, подобно любому другому объекту. Кроме того, эта функция порождает и более серьезную проблему, поскольку возвращает ссылку на result, но result – это локальный объект, а локальные объекты разрушаются при завершении функции, в которой они объявлены. Таким образом, эта версия operator* возвращает ссылку не на Rational, а на бывший Rational – пустой, отвратительный, гнилой скелет того, что когда-то было объектом Rational, но уже не является таковым, потому что он уничтожен. Стоит вызвать эту функцию – вы попадете в область неопределенного поведения. Запомним: любая функция, которая возвращает ссылку на локальный объект, некорректна (то же касается и функций, возвращающих указатель на локальный объект).
А теперь давайте рассмотрим возможность конструирования объекта в «куче» с возвратом ссылки на него. Объекты в «куче» создаются посредством new. Вот как мог бы выглядеть operator* в этом случае:
const Rational& operator*(const Rational& lhs, // предупреждение!
const Rational& rhs) // Опять плохой код!
{
Rational *result = new Rational(lhs.n * rhs.h, lhs.d * rhs.d);
return *result;
}
Да, вам все же придется расплачиваться за вызов конструктора, поскольку память, выделяемая new, инициализируется вызовом соответствующего конструктора, но теперь возникает новая проблема: кто выполнит delete для объекта, созданного вами с использованием new?
Даже если вызывающая программа написана аккуратно и добросовестно, не вполне понятно, как она предотвратит утечку в следующем вполне естественном сценарии:
Rational w, x, y, z;
w = x * y * z; // то же, что operator*(operator*(x, y), z)
Здесь выполняется два вызова operator* в одном предложении, поэтому получаются два вызова new, которым должны соответствовать два delete. Но у пользователя operator* нет возможности это сделать, так как он не может получить указатели, скрытые за ссылками, которые возвращает функция operator*. Это гарантированная утечка ресурсов.
Но, возможно, вы заметили, что оба подхода (на основе стека и на основе кучи) страдают от необходимости вызова конструкторов для каждого возвращаемого значения operator*. Вспомните, что исходно мы ставили себе целью вообще не вызывать конструкторы. Быть может, вы думаете, что знаете, как избежать всего, всех вызовов конструктора, кроме одного. Не исключено, что вы придумали следующую реализацию функции operator*, которая возвращает ссылку на
const Rational& operator*(const Rational& lhs, // предупреждение!
const Rational& rhs) // Код еще хуже!
{
static Rational result; // статический объект,
// на который возвращается ссылка
result = ...; // умножить lhs на rhs и поместить
// произведение в result
return result;
}
Подобно всем проектным решениям на основе статических объектов, это сразу вызывает вопросы, связанные с безопасностью относительно потоков, но есть и более очевидный недостаток. Чтобы разглядеть его, рассмотрим следующий абсолютно разумный код:
bool operator==(const Rational& lhs, // оператор == для Rational
const Rational& rhs);
Rational a, b, c, d;