Шрифт:
Другая причина того, почему маленькие пользовательские типы не обязательно хороши для передачи по значению, заключается в том, что их размер подвержен изменениям. Тип, который мал сегодня, может вырасти в будущем, потому что его внутренняя реализация может измениться. Ситуация меняется даже в том случае, если вы переключаетесь на другую реализацию C++. Например, в одних реализациях тип string из стандартной библиотеки в семь раз больше, чем в других.
Вообще говоря, единственные типы, для которых можно предположить, что передача по значению будет недорогой, – это встроенные типы, а также итераторы и функциональные объекты STL. Для всего остального следуйте совету этого правила и передавайте параметры по ссылке на константу вместо передачи по значению.
• Передаче по значению предпочитайте передачу по ссылке на константу. Обычно это более эффективно и позволяет избежать проблемы «срезки».
• Это правило не касается встроенных типов, итераторов и функциональных объектов STL. Для них передача по значению обычно подходит больше.
Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект
Как только программисты осознают проблемы эффективности, связанные с передачей объектов по значению (см. правило 20), они, подобно крестоносцам, преисполняются решимости искоренить зло – передачу по значению – везде, где бы оно ни пряталось. Непреклонные в своем «святом» порыве, они с неизбежностью допускают фатальную ошибку: начинают передавать по ссылке значения несуществующих объектов. А это неправильно.
Рассмотрим класс для представления рациональных чисел, включающий в себя дружественную функцию для перемножения двух таких чисел:
Ясно, что эта версия operator* возвращает результирующий объект по значению, и вы обнаружили бы непрофессиональный подход, если бы не уделили внимания вопросу о затратах на создание и удаление объекта. Вы не хотите платить за то, за что платить не должны. Отсюда вопрос: должны ли вы платить?
Нет, если можете вернуть ссылку. Но ссылка – это просто другое имя некоторого существующего объекта. Всякий раз, сталкиваясь с объявлением ссылки, вы должны спросить себя: для чего предназначено это имя, ведь оно должно принадлежать чему-то. В случае operator*, если функция возвращает ссылку, значит, она должна вернуть ссылку на некоторый уже существующий объект Rational, который и содержит произведение двух объектов, которые следовало перемножить.
Очевидно, нет никаких оснований полагать, что такой объект существует до вызова operator*. Например, если у вас есть
то неразумно ожидать, что уже существует то рациональное число со значением три десятых. Если operator* будет возвращать такое число, то он должен создать его самостоятельно.
Функция может создать новый объект только двумя способами: в стеке или в куче. Создание в стеке осуществляется посредством определения локальной переменной. Используя эту стратегию, вы можете попытаться написать operator* так:
Этот подход можно отвергнуть сразу, потому что вашей целью было избежать вызова конструктора, а result должен быть создан, подобно любому другому объекту. Кроме того, эта функция порождает и более серьезную проблему, поскольку возвращает ссылку на result, но result – это локальный объект, а локальные объекты разрушаются при завершении функции, в которой они объявлены. Таким образом, эта версия operator* возвращает ссылку не на Rational, а на бывший Rational – пустой, отвратительный, гнилой скелет того, что когда-то было объектом Rational, но уже не является таковым, потому что он уничтожен. Стоит вызвать эту функцию – вы попадете в область неопределенного поведения. Запомним: любая функция, которая возвращает ссылку на локальный объект, некорректна (то же касается и функций, возвращающих указатель на локальный объект).
А теперь давайте рассмотрим возможность конструирования объекта в «куче» с возвратом ссылки на него. Объекты в «куче» создаются посредством new. Вот как мог бы выглядеть operator* в этом случае:
Да, вам все же придется расплачиваться за вызов конструктора, поскольку память, выделяемая new, инициализируется вызовом соответствующего конструктора, но теперь возникает новая проблема: кто выполнит delete для объекта, созданного вами с использованием new?
Даже если вызывающая программа написана аккуратно и добросовестно, не вполне понятно, как она предотвратит утечку в следующем вполне естественном сценарии: