С. Мейерс - Эффективный и современный C++ (1114942), страница 46
Текст из файла (страница 46)
Если вы читали его давно или вовсе не читали, просмотрите указанный раздел, прежде чем читать данный.Отказ от пе р еr рузк иВ первом примере раздела 5.4 функция logAndAdd является типичным представителем функций, которые могут избежать недостатков перегрузки для универсальныхссылок, просто используя разные имена для потенциальных перегрузок. Например,рассматривавшиеся перегрузки l ogAndAdd могут быть разделены на logAndAddNameи logAndAddNameidx. Увы, этот подход не будет работать для второго рассматривавшегося примера - конструктора Person, потому что имена конструкторов в языке зафиксированы.
Кроме того, кто же захочет отказаться от перегрузки�Пе р едача cons t т &Одной из альтернатив является возврат к С++98 и замена передачи универсальнойссылки передачей lvаluе-ссылки на const. Фактически это первый подход, рассматривавшийся в разделе 5.4. Его недостаток состоит в том, что данный дизайн не столь эффективен, как нам хотелось бы. С учетом всех нынешних наших знаний о взаимодействии универсальных ссылок и перегрузки отказ от некоторой эффективности в пользу простотыможет оказаться более привлекательными компромиссом, чем нам казалось изначально.Пе редача по значен и юПодход, который часто позволяет добиться производительности без увеличениясложности, заключается в замене передачи параметров по ссылке передачей по значению,как бы противоестественно это ни звучало. Этот дизайн основан на совете из раздела 8.
1 :рассмотреть вопрос о передаче объектов п о значению в случае, когда известно, что онибудут копироваться. Поэтому я отложу до указанного раздела подробное обсуждениетого, как все это работает и насколько эффективным является это решение. Здесь я просто покажу, как этот подход может использоваться в примере с классом Person:class PersonpuЫ i c :1 90Глава 5.
Rvаluе-ссылки, семантика перемещений и п рямая передачаexpl icit Person ( std : : string n ) / ///: name ( std : : move ( n ) ) { 111//explicit Person ( int idx f: name ( nameFromidx ( idx ) ) 1 1Замена конструктора с Т & & ;о применении std: : moveчитайте в разделе 8 . 1Как и ранееprivate :std : : string name ;f;Поскольку конструктора s t d : : st r i ng, принимающего только целочисленное значение,нет, все аргументы типа i nt и подобных ему (например, std : : s i ze_ t, short, l ong), передаваемые конструктору Person, будут перенаправлены к перегрузке для int.
Аналогично всеаргументы типа std : : string (а также аргументы, из которых могут быть созданы объектыstd: : string, например литералы наподобие "Ruth" ) будут передаваться конструктору, принимающему std : : string. Здесь для вызывающего кода нет никаких сюрпризов. Возможно,некоторые программисты будут удивлены, что применение О или NULL в качестве нулевогоуказателя приведет к вызову перегрузки для int, но таким программистам необходимо внимательно прочесть раздел 3.2 и читать его до полного просветления в данном вопросе.Д и сп етчериза ция де с кри п торовНи передача lvаluе-ссылки на const, ни передача по значению не предоставляют поддержку прямой передачи.
Если мотивом использования универсальной ссылки являетсяпрямая передача, мы вынуждены использовать универсальную ссылку; у нас просто нетиного выбора. Тем не менее мы не хотим отказываться и от перегрузки. Если мы не отказываемся ни от перегрузок, ни от универсальных ссылок, то как же мы сможем избежатьперегрузки для универсальных ссылок?В действительности это не так трудно.
Вызовы перегруженных функций разрешаютсяпутем просмотра всех параметров всех перегрузок, а также всех аргументов в точке вызова с последующим выбором функции с наилучшим общим соответствием - с учетомвсех комбинаций "параметр/аргумент". Параметр, являющийся универсальной ссылкой,обычно обеспечивает точное соответствие для всего, что бы ни было передано, но еслиуниверсальная ссылка является частью списка параметров, содержащего другие параметры, универсальными ссылками не являющиеся, достаточно плохое соответствие этихпоследних параметров может привести к отказу от вызова такой перегрузки. Эта идея лежит в основе подхода диспетчеризации дескрипторов (tag dispatch), а приведенный нижепример позволит лучше понять, о чем идет речь.Применим этот метод к примеру logAndAdd из третьего фрагмента кода раздела 5.4.Чтобы вам не пришлось его искать, повторим его здесь:std : : multiset<std : : string> name s ; / / Глобальная структура данныхtemplate<typename Т>void logAndAdd ( T & & name )11 Делает запись в журнале и11 добавляет name в names5.5.
Знакомство с альтернативами перегрузки для универсальных ссылок191auto nowstd : : chrono : : system_clock: : now ( ) ;log ( now, " logAndAdd" ) ;names . emplace ( std : : forward<T> ( name ) ) ;=Сама по себе эта функция работает отлично, но если мы добавим перегрузку, принимающую значение типа int, использующееся для поиска объекта по индексу, то получим проблемы, описанные в разделе 5.4. Цель данного раздела - их избежать.
Вместо добавленияперегрузки мы реализуем l ogAndAdd заново для делегирования работы двум другим функциям: одной - для целочисленных значений, а другой - для всего прочего. Сама функцияlogAndAdd будет принимать все типы аргументов, как целочисленные, так и нет.Эти две функции, выполняющие реальную работу, будут называться logAndAdd impl,т.е. мы воспользуемся перегрузкой. Одна из этих функций будет принимать универсальную ссылку. Так что у нас будет одновременно и перегрузка, и универсальные ссылки.
Нокаждая функция будет принимать и второй параметр, указывающий, является ли передаваемый аргумент целочисленным значением. Этот второй параметр и будет средством,избавляющим нас от падений в болото, описанное в разделе 5.4, так как он будет фактором, определяющим выбираемую перегрузку.Что вы говорите? "Хватит трепа, переходи к делу"? Да хоть сию секунду! Вот почтикорректная версия обновленной функции logAndAdd:template<typename Т>void logAndAdd (T&& name ){logAndAddimpl ( st d : : forward<T> ( name ) ,std: : is integral<Т> ( ) ) ; / / Не совсем корректно_Эта функция передает свой параметр в logAndAddi mpl и при этом передает также аргумент, указывающий, является ли тип параметра (Т) целочисленным.
Как минимумэто то, что она должна делать. То же самое она делает и для целочисленных аргументов, являющихся rvalue. Но, как поясняется в разделе 5.6, если lvаluе-аргумент передается универсальной ссылке name, то выведенный тип для т будет Ivаluе-ссылкой. Такчто если в функцию l ogAndAdd передается lvalue типа i n t , то тип Т будет выведен какint &.
Но это не целочисленный тип - ссылка таковым не является. Это означает, чтоstd : : i s_i n t e g ra l <T> будет иметь ложное значение для любого lvаluе-аргумента, дажеесли этот аргумент на самом деле является целочисленным значением.Понимание данной проблемы равносильно ее решению, поскольку в стандартной библиотеке имеется такое средство, как s t d : : remove_refe rence (см. раздел 3.3), котороеделает то, о чем говорит его имя и в чем мы так нуждаемся: удаляет любые квалификаторы ссылок из типа. Так что верный способ написания l ogAndAdd имеет следующий вид:template<typename Т>void logAndAdd (T&& name )192Глава 5 .
Rvаluе-ссылки, семанти ка перемещений и п рямая передачаlogAndAddimpl (std : : forward<T> ( name ) ,std : : i s integral<typename std: : remove reference<T>: : type_>());Это в определенной мере трюк. (Кстати, в С++ 14 можно сэкономить несколько нажатийклавиш, воспользовавшись вместо выделенного текста st d : : remove r e f e r e nce t <T>.Подробнее об этом рассказывается в разделе 3.3.)После принятия этих мер мы можем перенести наше внимание к вызываемой функции, l ogAndAdd i mp l . Имеются две перегрузки, первая из которых примен има к любому нецелочисленному типу (т.е. ко всем типам, для которых значениеstd : : i s integra l < t ypename std : : remove_reference<T> : : type> ложно):11 Нецелочисленный аргумент добавляется11 в глобальную структуру данных :template<typename Т>void logAndAddimpl (T&& name , s t d : : fa lse t ype )_{auto nowstd : : chrono : : system_clock : : now ( ) ;l og ( now, " logAndAdd " ) ;names .