Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Все объекты класса Window имеют имя, которое вы можете получить посредством функции name,
Теперь предположим, что вы хотите написать функцию, которая будет печатать имя окна и затем отображать его. Вот неверный способ написания такой функции:
Посмотрим, что случится, если вызвать эту функцию, передав ей объект WindowWithScrollBar:
Параметр w будет сконструирован – он передан по значению, помните? – как объект Window, и вся дополнительная информация, которая делает его объектом WindowWithScrollBar, будет срезана. Внутри printNameAndDisplay w всегда будет вести себя как объект класса Window (потому что это и есть объект класса Window), независимо от типа объекта, в действительности переданного функции. В частности, вызов функции display внутри printNameAndDisplay всегда вызовет Window::display и никогда – WindowWithScrollBar::display.
Способ решения проблемы «срезки» – передать w по ссылке на константу:
Теперь w ведет себя правильно, какое бы окно он ни представлял в действительности.
Если вы заглянете «под капот» C++, то увидите, что ссылки обычно реализуются как указатели, поэтому передача чего-либо по ссылке обычно означает передачу указателя. В результате объекты встроенного типа (например, int) всегда более эффективно передавать по значению, чем по ссылке. Поэтому для встроенных типов, если у вас есть выбор – передавать по значению или по ссылке на константу, имеет смысл выбрать передачу по значению. Тот же совет касается итераторов и функциональных объектов STL, потому что они специально спроектированы для передачи по значению. Программисты, реализующие итераторы и функциональные объекты, отвечают за то, чтобы обеспечить эффективность передачи их по значению и исключить «срезку». Это пример того, как меняются правила в зависимости от используемой вами части C++ (см. правило 1).
Встроенные типы являются небольшими объектами, поэтому некоторые делают вывод, что все встроенные типы – хорошие кандидаты на передачу по значению, даже если они определены пользователем. Сомнительно. То, что объект небольшой, еще не значит, что вызов его конструктора копирования обойдется дешево. Многие объекты – среди них большинство контейнеров STL – содержат в себе немногим больше обычного указателя, но копирование таких объектов влечет за собой копирование всего, на что они указывают. Это может оказаться очень дорого.
Даже когда маленькие объекты имеют ненакладные конструкторы копирования, все равно они могут оказывать влияние на производительность. Некоторые компиляторы рассматривают встроенные и пользовательские типы по-разному, даже если они имеют одинаковое внутреннее представление. Например, некоторые компиляторы не размещают объекты, состоящие из одного лишь double в регистрах, даже если готовы размещать там значения встроенного типа double. В таких случаях лучше передавать объекты по ссылке, потому что компилятор безусловно готов поместить в регистр указатель (реализующий ссылку).
Другая причина того, почему маленькие пользовательские типы не обязательно хороши для передачи по значению, заключается в том, что их размер подвержен изменениям. Тип, который мал сегодня, может вырасти в будущем, потому что его внутренняя реализация может измениться. Ситуация меняется даже в том случае, если вы переключаетесь на другую реализацию C++. Например, в одних реализациях тип string из стандартной библиотеки в семь раз больше, чем в других.
Вообще говоря, единственные типы, для которых можно предположить, что передача по значению будет недорогой, – это встроенные типы, а также итераторы и функциональные объекты STL. Для всего остального следуйте совету этого правила и передавайте параметры по ссылке на константу вместо передачи по значению.
• Передаче по значению предпочитайте передачу по ссылке на константу. Обычно это более эффективно и позволяет избежать проблемы «срезки».
• Это правило не касается встроенных типов, итераторов и функциональных объектов STL. Для них передача по значению обычно подходит больше.
Правило 21: Не пытайтесь вернуть ссылку, когда должны вернуть объект
Как только программисты осознают проблемы эффективности, связанные с передачей объектов по значению (см. правило 20), они, подобно крестоносцам, преисполняются решимости искоренить зло – передачу по значению – везде, где бы оно ни пряталось. Непреклонные в своем «святом» порыве, они с неизбежностью допускают фатальную ошибку: начинают передавать по ссылке значения несуществующих объектов. А это неправильно.
Рассмотрим класс для представления рациональных чисел, включающий в себя дружественную функцию для перемножения двух таких чисел:
Ясно, что эта версия operator* возвращает результирующий объект по значению, и вы обнаружили бы непрофессиональный подход, если бы не уделили внимания вопросу о затратах на создание и удаление объекта. Вы не хотите платить за то, за что платить не должны. Отсюда вопрос: должны ли вы платить?
Нет, если можете вернуть ссылку. Но ссылка – это просто другое имя некоторого существующего объекта. Всякий раз, сталкиваясь с объявлением ссылки, вы должны спросить себя: для чего предназначено это имя, ведь оно должно принадлежать чему-то. В случае operator*, если функция возвращает ссылку, значит, она должна вернуть ссылку на некоторый уже существующий объект Rational, который и содержит произведение двух объектов, которые следовало перемножить.