Даже когда маленькие объекты имеют ненакладные конструкторы копирования, все равно они могут оказывать влияние на производительность. Некоторые компиляторы рассматривают встроенные и пользовательские типы по-разному, даже если они имеют одинаковое внутреннее представление. Например, некоторые компиляторы не размещают объекты, состоящие из одного лишь double в регистрах, даже если готовы размещать там значения встроенного типа double. В таких случаях лучше передавать объекты по ссылке, потому что компилятор безусловно готов поместить в регистр указатель (реализующий ссылку).
Другая причина того, почему маленькие пользовательские типы не обязательно хороши для передачи по значению, заключается в том, что их размер подвержен изменениям. Тип, который мал сегодня, может вырасти в будущем, потому что его внутренняя реализация может измениться. Ситуация меняется даже в том случае, если вы переключаетесь на другую реализацию C++. Например, в одних реализациях тип string из стандартной библиотеки в семь раз больше, чем в других.
Вообще говоря, единственные типы, для которых можно предположить, что передача по значению будет недорогой, – это встроенные типы, а также итераторы и функциональные объекты STL. Для всего остального следуйте совету этого правила и передавайте параметры по ссылке на константу вместо передачи по значению.
Что следует помнить
• Передаче по значению предпочитайте передачу по ссылке на константу. Обычно это более эффективно и позволяет избежать проблемы «срезки».
• Это правило не касается встроенных типов, итераторов и функциональных объектов 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, который и содержит произведение двух объектов, которые следовало перемножить.
Очевидно, нет никаких оснований полагать, что такой объект существует до вызова 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, но уже не является таковым, потому что он уничтожен. Стоит вызвать эту функцию – вы попадете в область неопределенного поведения. Запомним: любая функция, которая возвращает ссылку на локальный объект, некорректна (то же касается и функций, возвращающих указатель на локальный объект).
Читать дальше
Конец ознакомительного отрывка
Купить книгу