С. Мейерс - Эффективный и современный C++ (1114942), страница 70
Текст из файла (страница 70)
Передачу по значению стоит рассматривать только для параметров с недорогим перемещением. Когда перемещение дешевое, стоимость дополнительного перемещения может быть приемлемой, но если это не так, то излишнее перемещение становится аналогичным излишнему копированию, а важность устранения излишнегокопирования и была основной причиной появления правила С++98 о нежелательности применения передачи по значению.4.
Вы должны рассматривать передачу по значению только для параметров, которыевсегда копируются. Чтобы увидеть, почему это важно, предположим, что перед копированием параметра в контейнер names функция addName проверяет, не слишком ли короткое (или длинное) имя передано. Если это так, запрос на добавление286Гла ва В. Тонкостиимени игнорируется. Реализация с передачей по значению может быть написанаследующим образом:class Widget {puЫic :void addName ( st d : : string newName ){if ( (newName .
length () >= minLen) &&(newName . length ( ) <= maxLen) )names . push_back ( s t d : : move (newName) ) ;priva te :std : : vector<std: : string> names ;};Эта функция берет на себя создание и уничтожение newName, даже если в namesничего не добавляется. Это цена, которую подход с передачей по ссылке платитьне должен.Даже когда вы имеете дело с функцией, выполняющей безусловное копирование копируемого типа с дешевым перемещением, бывают моменты, когда передача по значениюможет оказаться неприемлемой.
Это связано с тем, что функция может копировать параметр двумя способами: с помощью конструирования (т.е. с помощью копирующего конструктора или перемещающего конструктора) и с помощью присваивания (т.е. с помощьюоператора копирующего или перемещающего присваивания). Функция addName использует конструирование: ее параметр newName передается функции vector : : push_back,и внутри этой функции newName конструируется копированием в новом элементе в конце вектора std : : vector.
Для функций, которые используют конструирование для копирования своего параметра, анализ, который мы видели ранее, завершен: использование передачи по значению приводит к дополнительному перемещению как для lvаluеаргументов, так и для rvаluе-аргументов.Когда параметр копируется с использованием присваивания, ситуация становитсяболее сложной. Предположим, например, что у нас есть класс, представляющий пароли.Поскольку пароль может изменяться, мы предоставляем функцию установки changeTo.Используя стратегию передачи по значению, реализовать E'assword можно следующимобразом:class E'assword {puЬl ic:explicit Password ( std: : string pwd) / / Передача: text (std: : move (pwd) ) { }п о значению;/ / конструирование textvoid changeTo (std: : string newPwd)/ / ПередачаВ.1.позначению;Рассмотрите передачу по значению дпя копируемых параметров"..287{ text=std: : move (newPwd) ; 1private :std : : string tex t ;11присваиваниеtext11 Текст пароля1;Хранение паролей в виде обычного текста приведет специалистов по безопасности в неистовство, но мы их проигнорируем и рассмотрим следующий код:std : : string initPwd ( "Supercal i fragi listicexpialidocious " ) ;Password p ( initPwd ) ;Здесь нет никаких сюрпризов: р .
text конструируется с использованием заданного пароля, а применение передачи по значению в конструкторе приводит к стоимости перемещающего конструирования std : : s t ring, которое может оказаться излишним при применении перегрузки или прямой передачи. Все в порядке.Пользователь этой программы может быть не столь оптимистичным насчет пароля,так как слово "Supercalifragilisticexpialidocious" можно найти в словаре.
А потому он предпринимает действия, которые ведут к выполнению следующего кода:std : : string newPassword = "Beware the Jabberwock" ;p . changeTo (newPassword ) ;Лучше новый пароль старого или нет - вопрос сложный, но это проблемы пользователя.Нашей же проблемой является то, что необходимость функции changeTo использоватьприсваивание (а не конструирование) для копирования параметра newPwd, вероятно,приведет к росту стоимости стратегии передачи параметра по значению.Аргумент, переданный функции changeTo, представляет собой lvalue ( newPassword) ,так что, когда конструируется параметр newPwd, вызывается копирующий конструкторs t d : : s t r i ng. Этот конструктор выделяет память для хранения нового пароля. ЗатемnewPwd присваивается с перемещением переменной t ext, что приводит к освобождениюпамяти, которая ранее принадлежала этой переменной text.
Таким образом, в changeToвыполняются два действия по управлению динамической памятью: одно выделяет память для хранения нового пароля, а второе освобождает память, в которой хранился старый пароль.Но в данном случае старый пароль ("Supercalifragilisticexpialidocious") длиннее нового("Beware the Jabberwock"), так что нет необходимости в выделении и освобождении памяти вовсе. Если бы использовался подход с перегрузкой, вероятно, никакие выделенияи освобождения не выполнялись бы:class Pas swordpuЫ i c :void changeTo ( con s t std : : string& newPwd) / / Перегрузка для1 1 lvaluetextnewPwd; / / При text . capacity ( ) >= newPwd .
size ( )1 1 можно использовать память text=288Гла ва 8. Тонкостиprivate :std: : s tring text ;/ / Как вьШJе};В этом сценарии стоимость передачи по значению включает дополнительное выделениеи освобождение памяти - стоимость, которая, скорее всего, превысит стоимость операции по перемещению std : : string на порядки.Интересно, что если старый пароль короче нового, то обычно невозможно избежатьпри присваивании действий по выделению и освобождению памяти, и в этом случае передача по значению будет выполняться практически с той же скоростью, что и передачапо ссылке. Таким образом, стоимость копирования параметров с помощью присваивания может зависеть от объектов, участвующих в присваивании! Этот вид анализа применим к любому типу параметров, который хранит данные в динамически выделеннойпамяти.
Не все типы таковы, но многие - включая std : : string и s t d : : vectorобладают этим свойством.Это потенциальное увеличение стоимости в общем случае применимо только к передаче аргументов, являющихся lvalue, поскольку необходимость выделения и освобождения памяти обычно возникает только тогда, когда выполняется истинное копирование(не перемещение). В случае rvаluе-аргументов перемещений почти всегда достаточно.Получается, что дополнительная стоимость передачи по значению (по сравнениюс передачей по ссылке) для функций, копирующих параметр с использованием присваивания, зависит от передаваемого типа, соотношения lvalue- и rvalue-apryмeнтoв и от того,использует ли тип динамическую память (и, если использует, то от реализации операторов присваивания для данного типа и вероятности того, что память, связанная с целевым объектом присваивания, как минимум того же размера, что и память присваиваемого объекта).
В случае std : : str ing она также зависит от того, использует ли реализацияоптимизацию малых строк (SSO; см. раздел 5.7), и если использует, то помещаются липрисваиваемые значения в буфер SSO.Так что, как я говорил, при копировании параметров с помощью присваивания анализ стоимости передачи по значению становится весьма сложным. Обычно наиболеепрактичный подход состоит в стратегии презумпции виновности ("виновен, пока не доказано иное"), в соответствии с которым вы используете перегрузку или универсальнуюссылку, а не передачу по значению, пока не будет показано, что передача по значениюдает приемлемо эффективный код для используемого вами типа параметра.Итак, для программного обеспечения, которое должно быть насколько это возможнобыстрым, передача по значению может оказаться неподходящей стратегией, посколькуважным может быть даже устранение дешевых перемещений.
Кроме того, не всегда понятно, сколько же перемещений имеют место на самом деле. В примере Widget : : addNameпередача по значению приводит только к одной лишней операции перемещения; однакопредположим, что W i dget : : addName вызывает Widget : : va l idateName, в которую параметр также передается по значению. (Возможно, имеется причина для того, чтобы всегда-8 . 1 . Рассмотрите передачу по значению дпя копируемых параметров"..289копировать параметр, например, для хранения его в структуре данных всех проверенныхзначений.) Предположим также, что функция val idateName вызывает третью функциюс передачей ей параметра по значению . . .Как видите, название раздела не зря такое неопределенное. Когда имеется цепочкавызовов функций, каждая из которых использует передачу по значению, поскольку "егостоимость составляет только одно недорогое перемещение': стоимость всей цепочки может стать такой, что вы не сможете спокойно ее терпеть.