С. Мейерс - Эффективный и современный C++ (1114942), страница 44
Текст из файла (страница 44)
Это означает, что "копирующая" версия ma keWidget на самом деле копирования не выполняет.Перемещающая версия ma keW idget делает только то, о чем говорит ее имя (в предположении наличия перемещающего конструктора Widget ) : она перемещает содержимое w2Такими локапьными объектами я1111яются бопьшинство покапьных переменных (например, такиекак w в ma keWidget ) , а также временные объекты, создаваемые как часть инструкции ret urn.Параметры функции на такое звание претендовать нс могут.
Некоторые программисты разпичаютприменение RVO к именованным и неимепованным (т.е. временным) локальным объектам, ограничивая термин "RVO" неименованными объектами и назы11ая его применение к именованнымобъектам оптимизацией именованных возвращаемых значений (named return value optimization-NRVO).5.3. Используйте std::move дл я rvalue-cc ыл o к, а std::forward - дл я универсальных ссылок181в местоположение возвращаемого значения makeWidget .
Но почему компиляторы не используют RVO для устранения перемещения, вновь создавая w в памяти, выделеннойдля возвращаемого значения функции? Ответ прост: они не могут. Условие (2) предусматривает, что RVO может быть выполнена, только если возвращается локальный объект,но в перемещающей версии ma keW i d g e t это не так. Посмотрим еще раз на инструкциюreturn:return std: : move (w) ;То, что здесь возвращается, не является локальным объектом w; это ссьтка на wрезультат s t d : : move ( w ) . Возврат ссылки на локальный объект не удовлетворяет условиям, требующимся для применения RVO, так что компиляторы вынуждены перемещать wв местоположение возвращаемого значения функции.
Разработчики, пытаясь с помощьюприменения s t d : : move к возвращаемой локальной переменной помочь компиляторамоптимизировать код, на самом деле ограничивают возможности оптимизации, доступные их компиляторам!Но RVOэто всего лишь оптимизация. Компиляторы не обязаны устранять операции копирования и перемещения даже тогда, когда это им позволено. Возможно, выпараноик и беспокоитесь о том, что ваши компиляторы будут выполнять операции копирования, просто потому, что они могут это делать.
А может, вы настолько глубоко разбираетесь в ситуации, что в состоянии распознать случаи, когда компиляторам трудноприменять RVO, например когда различные пути выполнения в функции возвращаютразные локальные переменные. (Компиляторы должны генерировать код для построениясоответствующей локальной переменной в памяти, выделенной для возвращаемого значения функции, но как компиляторы смогут определить, какая локальная переменнаядолжна использоватьсяn В таком случае вы можете быть готовы заплатить цену перемещения как гарантию того, что копирование выполнено не будет. Иначе говоря, вы можете продолжать думать, что применение s t d : : move к возвращаемому локальному объектуразумно просто потому, что при этом вы спокойны, зная, что вам не придется платить закопирование.В этом случае применение std : : move к локальному объекту все равно остается плохой идеей. Часть стандарта, разрешающая применение RVO, гласит далее, что если условия для применения RVO выполнены, но компиляторы предпочитают не выполнятьудаление копирования, то возвращаемый объект должен рассматриваться как rvalue.
Посути, стандарт требует, чтобы, когда оптимизация RVO разрешена, к возвращаемому локальному объекту либо применялось удаление копирования, либо неявно применяласьфункция s t d : : move. Так что в "копирующей" версии makeWidget--Widget makeWidget ( )1 1 Какиранее{Widget w ;return w;182Гnава 5. Rvalue-ccыnки, семантика перемещений и п рямая передачакомпиляторы должны либо устранить копирование w, либо рассматривать функцию, какесли бы она была написана следующим образом:Widget makeWidget ( )(Widget w;return std: : move(w) ; / / Рассматривает w как rvalue , поскольку/ / удаление копирования не выполняетсяСитуация аналогична для параметров функции, передаваемых по значению.
Они неимеют права на удаление копирования при их возврате из функции, но компиляторыдолжны рассматривать их в случае возврата как rvalue. В результате, если ваш исходныйтекст выглядит какWidget makeWidget (Wiclget w ) / / Передаваемый по значению параметр// имеет тот же тип, что и1 1 возвращаемый тип функции{return w;компиляторы должны рассматривать его, как если бы он был написан какWidget makeWidget (Widget w ){return std: : move (w) ;11 w рассматривается как rvalueЭто означает, что, используя s t d : : move для локального объекта, возвращаемого функцией по значению, вы не можете помочь компилятору (он обязан рассматривать локальныйобъект как rvaJue, если не выполняет удаления копирования), но вы, определенно, в состоянии ему помешать (препятствуя RVO).
Есть ситуации, когда применение s t d : : moveк локальной переменной может быть разумным (т.е. когда вы передаете ее функции и знаете, что больше вы ее использовать не будете), но эти ситуации не включают применениеs t d : : rnove в качестве части инструкции return, которая в противном случае претендовалабы на оптимизацию RVO, или возврат параметра, передаваемого по значению.Сnедует запомнить•Применяйте s t d : : move к rvаluе-ссылкам, а s t d : : forwardкам, когда вы используете их в последний раз.•Делайте то же для rvalue- и универсальных ссылок, возвращаемых из функцийпо значению.•Никогда не применяйте s t d : : move и s t d : : forward к локальным объектам, которыемогут быть объектом оптимизации возвращаемого значения.5.3 . Испол ьзуйте std::move АЛА rvalue-ccы л oк, а std::forward--к универсальным ссылАЛА универсальных ссы л ок1 83S .4.
И збе rайте пере r рузок дnя универсаnьных ссыnокПредположим, что вам надо написать функцию, которая принимает в качестве параметра имя, записывает в журнал текущие дату и время, а затем добавляет имя в глобальную структуру данных. Вы могли бы начать с функции, которая имеет примерноследующий вид:std: : multiset<std : : s triпg> пames ; / / Глобальная структура данныхvoid logAndAdd ( const std: : s tring& пате ){auto поw11 Получение текущего времениstd: : chroпo : : system_clock : : поw ( ) ;/ / Создание журнальной записиlog ( пow, " logAndAdd " ) ;пames .
emplace (пame ) ;11 Добавление паmе в глобальную1 1 структуру даннь� ; emplace1 1 см. в разделе 8 . 2=Этот код не является неразумным, но он не такой эффективный, каким мог бы быть. Рассмотрим три потенциальных вызова:std: : st ring petName ( "Darla" ) ;11 lvalue типа std : : striпglogAndAdd (petName) ;logAndAdd ( std: : strinq ( "Persephone") ) ; // rvalue типа std : : striпglogAndAdd ( "Patty Doq" ) ;/ / Строковый литералВ первом вызове параметр паmе функции 1 ogAпdAdd связывается с переменной petName. Внутри logAпdAdd параметр паmе в конечном итоге передается в вызовпames . emplace.
Поскольку паmе является lvalue, он копируется в пames. Избежать этогокопирования невозможно, так как lvalue (petName) передается в функцию logAпdAdd.Во втором вызове параметр name связывается с гvalue (временный объектstd : : st r i пg, явно созданный из строки " Persephoпe " ) . Параметр паmе сам по себе является lvalue, так что он копируется в пames, но мы отдаем себе отчет, что, в принципе,это значение может быть перемещено в пames. В этом вызове мы платим за копирование,но мы должны быть способны сделать то же с помощью перемещения.В третьем вызове параметр паmе опять связывается с rvalue, но в этот раз со временным объектом s t d : : s t r iпg, который неявно создается из " Patty Dog'' . Как и во втором вызове, паmе копируется в names, но в этот раз аргумент, изначально переданныйв logAпdAdd, был строковым литералом.
Если бы строковый литерал непосредственнопередавался в emplace, в создании временного объекта std : : str i пg не было бы необходимости вообще. Вместо этого функция emplace использовала бы строковый литералдля создания объекта std : : str ing непосредственно в std : : mu l t i set. Таким образом,в этом третьем вызове мы платим за копирование std : : st ri ng, при том что нет причинплатить даже за перемещение, не говоря уже о копировании.Неэффективность второго и третьего вызовов logAndAdd можно устранить, переписав эту функцию так, чтобы она принимала универсальную ссылку (см. раздел 5.2) и,1 84Глава S . Rvа l uе-ссылки , семантика перемещений и прямая передачасогласно разделу 5.3, передавала ее с помощью s t d : : f o rward функции ernp l a ce .
Результат говорит сам за себя:tamplate<t:ypen.ame Т>void logAndAdd (T&& name ){auto now = std : : chrono : : system_clock : : now ( ) ;log ( now, " logAndAdd " ) ;names . emplace ( std : : forward<Т> (name ) ) ;std : : string petName ( " Darla " ) ;11 Как и ранееlogAndAdd (petNaшe) ;11 Как и ранее , копирова// ние lvalue в multisetlogAndAdd (std: : strinq ( "Persephone") ) ; / / Перемещение rvalue11 вместо копированияlogAndAdd ( "Patty Dog" ) ;11 Соэдание s td : : string1 1 в mult i set вместо/ /копирования временного11 s t d : : st ringУра, получена оптимальная эффективность!Если бы это был конец истории, мы могли бы остановиться и гордо удалиться, ноя не сказал вам, что клиенты не всегда имеют непосредственный доступ к именам, требующимся logAndAdd.
Некоторые клиенты имеют только индекс, который l ogAndAdd использует для поиска соответствующего имени в таблице. Для поддержки таких клиентоввыполняется перегрузка функции logAndAdd:std : : string nameFromidx ( int idx ) ; / / Возвращает имя,1 1 соответствующее idx/ / Новая перегрузкаvoid logAndAdd ( int iclx){auto nows t d : : chrono : : system_clock : : now ( ) ;log ( now, " logAndAdd " ) ;name s . emplace (nameFroшiclx (iclx) ) ;=Разрешение перегрузки работает, как и следовало ожидать:std: : s tring petName ( " Darla" ) ;1 1 Как и ранее11 Как и ранее, эти вызовыlogAndAdd (petName ) ;logAndAdd ( s td : : string ( " Persephone " ) ) ; 11 приводят к испольэова// нию перегрузки для Т&&logAndAdd ( " Patty Dog" ) ;logAndAdd ( 2 2 ) ;1 1 Вызов int -перегруэкиS.4. Избегайте перегрузок дnя ун мверсаnьных ссыпок1 85На самом деле разрешение работает, как ожидается, только если вы не ожидаете слиш ком многого.
Предположим, клиент имеет переменную типа short , хранящую индекс,и передает ее функции logAndAdd:short name idx ;11 Дает значение переменной name idxlogAndAdd ( namei dx ) ; // Ошибка 1Комментарий в последней строке, может быть, не слишком понятен, так что позвольтемне пояснить, что же здесь произошло.Имеется две перегрузки logAndAdd.
Одна из них, принимающая универсальную ссылку, может вывести тип Т как short, тем самым приводя к точному соответствию. Перегрузка с параметром int может соответствовать аргументу short только с повышением.Согласно обычным правилам разрешения перегрузки точное соответствие побеждаетсоответствие с повышением, так что вызывается перегрузка для универсальной ссылки.В этой перегрузке параметр name связывается с переданным значением типа short.Таким образом, name передается с помощью s t d : : forward функции-члену emplace объекта name s ( s t d : : mu l t i s e t < s t d : : s t r i n g > ) , которая, в свою очередь, послушно передает его конструктору s t d : : s t ri ng.
Но конструктора s t d : : s t r i ng, который принималбы значение short, не существует, так что вызов конструктора s t d : : s t r i ng в вызовеmul t i se t : : emplace в вызове logAndAdd неудачен. Все дело в том, что перегрузка для универсальной ссылки точнее соответствует аргументу типа short, чем перегрузка для int.Функции, принимающие универсальные ссылки, оказываются самыми жадными в С++.Они в состоянии выполнить инстанцирование с точным соответствием практическидля любого типа аргумента (несколько видов аргументов, для которых это не так, описаны в разделе 5.8).