С. Мейерс - Эффективный и современный C++ (1114942), страница 43
Текст из файла (страница 43)
Этот видповедения может привести программиста к отчаянию - если не к прямому насилию.Можно возразить, что setName не должен был объявлять свой параметр как универсальную ссылку. Такие ссылки не могут быть константными (см. раздел 5.2), но setName,безусловно, не должен изменять свой параметр. Вы могли бы указать, что если перегрузить setName для константных значений lvalue и rvalue, то этой проблемы можно былобы избежать, например, таким образом:class Widget {puЬlic :void setName ( const std: : string& newName ){ name = newNarne ; }void setName (std: : string&& newName)s t d : : rnove ( newName ) ; }{ name=/ / Устанавливается// из const lvalue// Устанавливается11 из rvalue};В данном случае это, безусловно, сработает, но у метода есть и недостатки.
Во-первых,требуется вводить исходный текст большего размера (две функции вместо одного5.3.Испоnьзуйте std::move дл я rvalue-cc ыл oк, а std::forward - дл я универсальных ссыпок1 77шаблона). Во-вторых, это может быть менее эффективным. Например, рассмотрим следующее применение setName:w . setName ( "Adela Nova k" ) ;При наличии версии setName, принимающей универсальную ссылку, функции setNameбудет передан строковый литерал "Adel a Nova k " , в котором он будет передан операторуприсваивания для s t d : : s t r ing внутри w. Таким образом, член-данные name объекта wбудет присвоен непосредственно из строкового литерала; никакого временного объектаs t d : : s t r i ng создаваться не будет.
Однако в случае перегруженных версий setName будет создан временный объект s t d : : s t r i ng, с которым будет связан параметр функцииset Name, и этот временный объект s t d : : s t r i ng будет перемещен в член-данные объекта w. Таким образом, вызов setN ame повлечет за собой выполнение одного конструктора s t d : : s t r ing (для создания временного объекта), одного перемещающего оператораприсваивания s t d : : st r ing (для перемещения newName в w .
name ) и одного деструктораs t d : : s t ring (для уничтожения временного объекта). Это практически наверняка болеедорогостоящая последовательность операций, чем вызов только одного оператора присваивания s t d : : s t r i ng, принимающего указатель cons t c h a r * . Дополнительная стоимость может варьироваться от реализации к реализации, и стоит ли беспокоиться о ней,зависит от приложения и библиотеки; однако, скорее всего, в ряде случаев замена шаблона, получающего универсальную ссылку, парой функций, перегруженных для lvalueи rvalue-ccылoк, приведет к дополнительным затратам времени выполнения.Однако наиболее серьезной проблемой с перегрузкой для lvalue и rvalue является необъем или идиоматичность исходного кода и не производительность времени выполнения. Это - плохая масштабируемость проекта.
Widget : : setName принимает только одинпараметр, так что необходимы только две перегрузки. Но для функций, принимающихбольшее количество параметров, каждый из которых может быть как lvalue, так и rvalue,количество перегрузок растет в соответствии с показательной функцией: п параметровтребуют 2" перегрузок. И это еще не самый худший случай. Некоторые функции (на самом деле - шаблоны функций) принимают неограниченное количество параметров, каждый из которых может быть как lvalue, так и rvalue. Типичными представителями такихфункций являются s t d : : ma ke_s h ared и, начиная с С++ 1 4, st d : : ma ke_uni que (см. раздел 4.4). Попробуйте написать объявления их наиболее часто используемых перегрузок:template<class Т, cla s s . .
. Args>/ / Из стандарта C++ l lshared_ptr<T> make shared (Args&& . . . args ) ;template<class Т, class . . . Args>11 Из стандарта С++ 1 4unique_ptr<T> make_unique (Args&& . . . args ) ;Для функций наподобие указанных перегрузка для lvalue и rvalue не является приемлемым вариантом: единственным выходом является применение универсальных ссылок.А внутри таких функций, уверяю вас, к универсальным ссылкам при их передаче другимфункциям следует применять s t d : : forwa rd. Вот то, что вы должны делать.1 78Глава S. Rvаl uе-ссылки, семантика перемещений и прямая п ередачаНу хорошо, обычно должны. В конечном итоге.
Но не обязательно изначально. В некоторых случаях вы захотите использовать привязку объекта к rvalue-ccылкe или универсальной ссылке более одного раза в одной функции, и вы захотите гарантировать,что перемещения не будет, пока вы явно не укажете его выполнить. В этом случае вы захотите применить std : : move (для rvalue-ccылoк) или std : : forward (для универсальныхссылок) только к последнему использованию ссылки, например:template<typename Т>void setSignText ( T & & text )11 text - универсальная11 ссылка{/ / Используем text, но не11 изменяем его/ / Получение текущего времениauto now =std : : chrono : : s ystem_clock : : now ( ) ;s ignHistory . add ( now,std: : forward<T> ( text) ) ; / / Условное приведение11 text к rvaluesign .
setText ( text) ;Здесь мы хотим гарантировать, что значение text не изменится вызовом s ign . setText,поскольку мы хотим использовать это значение при вызове s ignHi s tory . add. Следовательно, std : : forward применяется только к последнему использованию универсальнойссылки.Для std : : move применяются те же рассуждения (т.е. надо применить s t d : : moveк rvalue-ccылкe только при ее последнем использовании), но важно отметить, что в некоторых редких случаях вы захотите вызвать std : : move_ if _noexcept вместо std : : move.Чтобы узнать, когда и почему, обратитесь к разделу 3.8.Если вы имеете дело с функцией, осуществляющей возврат по значению, и возвращаете объект, привязанный к rvalue-ccылкe или универсальной ссылке, вы захотите применять std : : move или s t d : : forward при возврате ссылки.
Чтобы понять, почему, рассмотрим функцию operator+ для сложения двух матриц, где о левой матрице точно известно, что она является rvalue (а следовательно, может повторно использовать свою памятьдля хранения суммы матриц):мatrix//operator+ (мatrix&& lhs , const{lhs += rhs ;return std: : move (lhs) ; / ///Возврат по значениюMatrix& rhs )Перемещение lhs ввозвращаемое значениеС помощью приведения lhs к rvalue в инструкции return (с помощью s t d : : move )lhs будет перемещен в местоположение возвращаемого функцией значения. Если опустить вызов std : : move,Matrix/ / Ка к и ранее aboveoperator+ (Matrix&& lhs , const Matrix& rhs ){5.3.Используйте std::move дл я rvalue-ccыnoк, а std::forward - дл я универсальных ссыпок1 79lhs += rhs ;returп lhs;/ / Копирование lhs в1 1 возвращаемое значението тот факт, что l h s представляет собой lvalue, заставит компиляторы вместо перемещени я копировать его в местоположение возвращаемого функцией значения.В предположении, что тип Mat r i x поддерживает перемещающее конструирование,более эффективное, чем копирующее, применение s t d : : move в инструкции returnдает более эффективный код.Если тип Mat rix не поддерживает перемещения, приведение его к rvalue не повредит, поскольку rvalue будет просто скопировано копирующим конструктором Mat rix (см.раздел 5.
1 ). Если Matrix позже будет переделан так, что станет поддерживать перемещение, operator+ автоматически использует данное преимущество при следующей компиляции. В таком случае ничто не будет потеряно (и возможно, многое будет приобретено)при применении s t d : : move к rvа\uе-ссылкам, возвращаемым из функций, которые осуществляют возврат по значению.Для универсальных ссылок и std : : forward ситуация схожа. Рассмотрим шаблонфункции reduceAndCopy, который получает возможно сократимую дробь Fract ion, сокращает ее, а затем возвращает копию сокращенной дроби. Если исходный объект представляет собой rvalue, его значение должно быть перенесено в возвращаемое значение(избегая тем самым стоимости создания копии), но если исходный объект - lvalue,должна быть создана фактическая копия:template<typeпame Т>FractionreduceAndCopy ( T & & frac)1 1 Возврат по значению// Универсальнал ссылка{frac .
reduce ( ) ;return std : : forward<T> ( frac ) ; / / Перемещение rvalue и11 копирование lvalue в1 1 возвращаемое значениеЕсли опустить вызов std : : forward, frac будет в обязательном порядке копироватьсяв возвращаемое значение reduceAndCopy.Некоторые программисты берут приведенную выше информацию и пытаются распространить ее на ситуации, в которых она неприменима. Они рассуждают следующимобразом: "если использование std : : move для параметра, являющегося rvаluе-ссылкойи копируемого в возвращаемое значение, превращает копирующий конструктор в перемещающий, то я могу выполнить ту же оптимизацию для возвращаемых мною локальных переменных".
Другими словами, они считают, что если дана функция, возвращающаялокальную переменную по значению, такая, как следующая:Widget makeWidget ( ) 1 1 "Коnирующал" версил makeWidget{Widqet w ;1 801 1 Переменнал1 1 Настройка wГлава S. Rvalue-cc ыnки, семантика перемещений и прямая передачаreturn w ;11"Копирование" w в возвращаемое значението они могут "оптимизировать" ее, превратив "копирование" в перемещение:Widget makeWidget ( )11Перемещающая версия makeWidget{Widget w ;return std: : move (w) ; / / Перемещение w в возвращаемое11 значение ( не делайте этого ! )Мое обильное использование кавычек должно подсказать вам, что эти рассуждения нелишены недостатков.
Но почему? Да потому что Комитет по стандартизации уже прошелэтот путь и давно понял, что "копирующая" версия makeWidget может избежать необходимости копировать локальную переменную w, если будет создавать ее прямо в памяти,выделенной для возвращаемого значения функции. Это оптимизация, известная как оптимизация возвращаемого значения (retшn value optimizationRVO) и с самого началаблагословленная стандартом С++.Формулировка такого благословения - сложное дело, поскольку хочется разрешитьтакое отсутствие копирования только там, где оно не влияет на наблюдаемое поведениепрограммы.
Перефразируя излишне сухой текст стандарта, это благословение на отсутствие копирования (или перемещения) локального объекта2 в функции, выполняющейвозврат по значению, дается компиляторам, если ( 1 ) тип локального объекта совпадаетс возвращаемым функцией и (2) локальный объект представляет собой возвращаемоезначение. С учетом этого вернемся к "копирующей" версии makeWidget:-Widget makeWidget ( ) // " Копирующая" версия makeWidget{Widget w ;return w ;/ / "Копирование" w в возвращаемое значениеЗдесь выполняются оба условия, и вы можете доверять мне, когда я говорю вам, чтокаждый приличный компилятор С++ будет использовать RVO для того, чтобы избежатькопирования w.