С. Мейерс - Эффективный и современный C++ (1114942), страница 69
Текст из файла (страница 69)
push_back ( s td : : forward<T> ( newName ) ) ;11 Применение std : : forward описано в разделе 5 . 31;Это приводит к уменьшению количества исходного текста, с которым приходится работать, но применение универсальных ссылок влечет за собой другие сложности. Будучишаблоном, реализация addName обычно должна располагаться в заголовочном файле. Вобъектном коде такой подход может дать несколько функций, так как инстанцирование будет выполняться по-разному не только для lvalue и rvalue, но и для s t d : : stringи типов, преобразуемых в std : : s t r i ng (см.
раздел 5.3). В то же время имеются типыаргументов, которые не могут быть переданы с помощью универсальной ссылки (см. раздел 5.8), и если клиент передаст аргументы некорректного типа, сообщения компилятораоб ошибках могут быть приводящими в трепет (см. раздел 5.5).Было бы неплохо, если бы имелся способ написания функций наподобие addName,таких, чтобы lvalue копировались, rvalue перемещались, при этом (в исходном текстеи объектном коде) имелась бы только одна функция и при этом можно было избежатьнеприятностей, связанных с универсальными ссылками. И такой способ есть. Все, чтоот вас требуется, - забыть об одном из первых правил, с которыми вы познакомилиськак программист на С++.
Это правило, гласящее, что следует избегать передачи пользовательских типов по значению. Для параметров наподобие newName в функциях наподобие addName передача по значению может быть вполне разумной стратегией.Перед тем как начать выяснение, почему передача по значению может быть хорошимрешением для newName и addName, посмотрим, как она может быть реализована:282Гл ава 8.
Тонкостиclass Widget [puЫic :void addName ( std: : string newName ){ names . push_back ( s td : :move (newName) ) ;)11 lvalue или11 rva lue ;11 перемещаем его;Единственной неочевидной частью этого кода является применение std : : move к параметру newName. Обычно s t d : : move используется с rvаluе-ссылками, но в данном случае мызнаем, что ( 1 ) newName представляет собой объект, полностью независимый от того, чтопередает вызывающая функция, так что изменение newName не влияет на вызывающуюфункцию, и (2) это последнее применение newName, так что его перемещение н икак невлияет на остальную часть функции.Тот факт, что существует только одна функция addName, поясняет, как мы избегаемдублирования кода - как исходного, так и объектного.
Мы не используем универсальную ссылку, так что данный подход не ведет к увеличению заголовочных файлов, странным неприятностям или непонятным сообщениям об ошибках. Но что можно сказатьоб эффективности такого дизайна? Мы же выполняем передачу по значению. Не слишком ли она дорога?В С++98 можно держать пари, что так и есть. Независимо от того, что передает вызывающая функция, параметр newName создается с помощью копирующего конструктора.Однако в С++ 1 1 newName будет создаваться с помощью копирующего конструированиятолько для lvalue.
В случае rvalue этот объект создается с помощью перемещающего конструктора. Вот, взгляните:Widget w;std : : string name ( " Bart" ) ;w . addName (name ) ;11 Вызов addNameсlvaluew . addName (name + "Jenne " ) ; / / Вызов addName11 ( см . ниже)сrvalueВ первом вызове addName (при передаче name ) параметр newName инициализируется значением lvalue. Поэтому объект newName создается путем копирования, так же, как этобыло бы в С++98. Во втором вызове newName инициализируется объектом std : : string,полученным в результате вызова оператора operator+ для std : : string (т.е.
выполненияоперации добавления). Этот объект представляет собой rvalue, и newName, таким образом, создается перемещением.Итак, lvalue копируются, а rvalue перемещаются, как мы и хотели. Здорово, правда?Здорово, но есть несколько моментов, которые следует иметь в виду. Для облегченияпонимания вспомним три рассмотренные версии функции addName:class Widget { // Подход 1 : перегрузка для lvaluepuЫic :void addName (const std: : string& newName )иrvalue8 .
1 . Рассмотрите передачу по значению дл я копируемых параметров""283{ names . push_back (newName ) ; }void addName (std: : string&& newName ){ names . push_back ( s td : : move (newName ) ) ;private :std: : vector<std : : string> names ;};class Widget { / / Подход 2 : применение универсальной ссьmкиpuЬlic :teмplate<t:ypename Т>void addName (T&& newName )names . push_bac k ( s td : : forward<T> (newName ) ) ;};class Widget { / / Подход 3 : передача по значениюpuЬli c :void addName (std: : string newName ){ names . push_bac k ( s td : : move (newName ) ) ;};Я буду говорить о первых двух версиях как о "подходе с передачей ссылки", посколькуони обе передают параметры по ссылке.Вот два сценария вызова, которые мы рассмотрели:Widget w;std: : string name ( " Bart " ) ;w .
addName ( name ) ;1 1 Передача lvaluew . addName (name + " Jenne " ) ; / / Передача rvalueДавайте теперь рассмотрим стоимость (в операциях копирования и перемещения) добавления имени в Widget для приведенных сценариев и каждой из трех рассмотренныхреализаций a ddName. Мы будем игнорировать оптимизирующие возможности компиляторов по удалению копирований и перемещений, поскольку такая оптимизация зависитот контекста и компилятора и с практической точки зрения на суть анализа не влияет.•Переrрузка.
Независимо от передачи lvalue или rvalue аргумент вызывающейфункции связан со ссылкой по имени newName. Это ничего не стоит в смысле операций копирования и перемещения. В перегрузке для lvalue newName копируетсяв W i dg e t : : n ame s . В перегрузке для rvalue объект перемещается.
Итоговая стоимость: одно копирование для lvalue, одно перемещение для rvalue.•Применение универсальной ссылки. Как и в случае перегрузки, аргумент вызывающей функции связан со ссылкой n e w N a m e . Эта операция бесплатна.284Глава 8. ТонкостиБлагодаря использованию std : : forward lvalue-apryмeнты std : : s t ring копируются в Widget : : names , в то время как rvalue-apryмeнты s t d : : st ring перемещаются.Итоговая стоимость для аргументов std : : st r i ng такая же, как и при перегрузке:одно копирование для lvalue, одно перемещение для rvalue.В разделе 5.3 поясняется, что если вызывающая фун кция передает аргумент,отличный от std : : s t r i ng, он будет передан в конструктор std : : s t r i ng, и этоможет привести к нулевому количеству копирований и перемещений std : : s t r ing.Так им образом, функции, получающие универсал ьные ссылки, оказываютсяуникально эффективными.
Однако это не влияет на выполняемый нами анализ, такчто для простоты мы будем предполагать, что вызывающая функция всегда передаетаргументы std : : string.•Независимо от передачи lvalue или rvalue должен быть сконструирован параметр newName. Если передано lvalue, это стоит одно копирование,если передано rvalue - одно перемещение. В теле функции newName безусловноперемещается в Widget : : names. Итоговая стоимость, таким образом, равна одномукопированию и одному перемещению для lvalue и двум перемещениям для rvalue.По сравнению с подходом с передачей ссылки мы получаем одно лишнее перемещение как для lvalue, так и для rvalue.Передача по значению.Взглянем еще раз на название раздела:Рассмотрите передачу по значению для копируемых па раметров, которые легко перемещаются и всегда копируются.Оно сформулировано таким образом не без причины.
Точнее, не без четырех причин.l. Вы должны всего лишь рассмотреть использование передачи по значению. Да,при этом требуется написать всего лишь одну функцию. Да, при этом в объектном коде генерируется только одна функция. Да, вы избегаете проблем, связанныхс применением универсальных ссылок. Но стоимость этого решения выше, чемстоимость альтернативных вариантов, и, как вы увидите далее, в некоторых случаях есть стоимость, которую мы еще не рассматривали.2. Рассмотрите передачу по значению только для копируемых параметров.
Параметры, не соответствующие этому условию, должны иметь типы, являющиеся только перемещаемыми, поскольку если они не копируемые, а функция всегда делаеткопию, такая копия должна создаваться с помощью перемещающего конструктора2. Вспомним, что преимущества передачи по значению перед перегрузкой заключаются в том, что при передаче по значению достаточно написать только однуфункцию.
Но для только перемещаемых типов нет необходимости предоставлятьперегрузку для lvalue, поскольку копирование lvalue влечет вызов копирующегоконструктора, который у таких типов отсутствует. Это означает, что требуется2 Для таких предложений хорошо бы иметь терминологию, отничающую копирование с помощьюкопирующего конструктора от копирования с помощью перемещающего конструктора.8 . 1 . Рассмотрите передачу по значению дпя копируемых параметров....285поддержка только аргументов, являющихся rvalue, и в таком случае решение на основе "переrрузки" требует только одну переrрузку, принимающую rvalue-ccылкy.Рассмотрим класс с данными-членом std : : un ique_pt r<std : : st ring> и функциюустановки для неrо. Тип std : : unique _ptr является только перемещаемым типом,так что подход с использованием "перегрузки" состоит из единственной функции.class WidgetpuЫ i c :void set P t r ( std : : unique_ptr<std: : string>&& ptr){ р = std : : move (ptr) ; )private :std: : unique_ptr<std: : string> р ;;)Вызывающая функция может использовать ее следующим образом:Widgetw.w;setPtr ( std: : make_unique<std: : string> ( "Modern с++") ) ;Здесь rvalue std : : unique_ptr<std : : st ring>, возвращаемое из s t d : : make_unique(см.
раздел 4.4), передается по ссылке в setPtr, где оно перемещается в данныечлен р. Общая стоимость составляет одно перемещение.Если бы set Ptr принимала параметры по значению,class WidgetpuЫic:void setptr ( std : : шli.que_ptr<std: : string> ptr ){ р = std: :move (ptr) ; ));то тот же вызов создавал бы параметр ptr перемещением, а затем ptr был быперемещен в данные-член р. Общая стоимость составила б ы два перемещения в два раза больше, чем при подходе с "перегрузкой':3.