• Это правило не касается встроенных типов, итераторов и функциональных объектов STL. Для них передача по значению обычно подходит больше.
Правило 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?
Даже если вызывающая программа написана аккуратно и добросовестно, не вполне понятно, как она предотвратит утечку в следующем вполне естественном сценарии: