С. Мейерс - Эффективный и современный C++ (1114942), страница 45
Текст из файла (страница 45)
Именно поэтому сочетание перегрузки и универсальной ссылки почтивсегда является плохой идеей: перегрузка для универсальных ссылок годится для гораздобольшего количества типов аргументов, чем обычно ожидает разработчик перегрузок.Простой способ свалиться в эту яму - написать конструктор с прямой передачей.Небольшое изменение функции l ogAndAdd демонстрирует эту проблему. Вместо написания свободной функции, которая принимает либо s t d : : s t ri ng, либо индекс, которыйможно использовать для поиска s t d : : s t ring, представим себе класс Person с конструкторами, которые выполняют те же действия:class Person {puЫ i c :template<typename Т>// Конструктор с прямой передачейexpl icit Person ( T & & n): name ( s td : : forward<T> ( n ) ) { } / / инициализирует члены-данныеexplicit Person ( int idx )/ / Конструктор с параметром int: name ( nameFromidx ( idx ) ) { }private :std: : string name ;};1 86Гnава 5.
Rvalue-cc ыnки, семантика перемещений и прямая передачаКак и в случае с logAndAdd, передача целочисленного типа, отличного от int (например, std : : s i ze_t, short, l ong и т.п.), будет вызывать перегрузку конструктора для универсальной ссылки вместо перегрузки для i nt, и это будет вести к ошибкам компиляции. Однако проблема гораздо хуже, поскольку в Person имеется больше перегрузок,чем видит глаз.
В разделе 3 . 1 1 поясняется, что при соответствующих условиях С++ будетгенерировать как копирующие, так и перемещающие конструкторы, и это так и будет,даже если класс содержит шаблонный конструктор, который при инстанцировании в состоянии дать сигнатуру копирующего или перемещающего конструктора. Если такимобразом генерируются копирующий и перемещающий конструкторы для Person, классPerson будет выглядеть, по сути, следующим образом:class Person [puЬlic :/ / Конструктор с прямой передачейtemplate<typename Т>explicit Person (T&& n ): name ( std : : forward<T> ( n ) ) [ )explicit Person ( int idx) ;/ / Конструктор от intPerson (const Person& rhs ) ; / / Копирующий конструкторPerson (Person&& rhs) ;/! ( сгенерирован компилятором)/ / Перемещающий конструктор// ( сгенерирован компилятором));Это приводит к поведению, интуитивно понятному, только если вы потратили на работу с компиляторами и общение с их разработчиками столько времени, что забыли,каково это - быть человеком:Person р ( "Nапсу" ) ;auto cloneOfP ( p ) ; / / Создание нового объекта Person из р ;1 1 этот код н е компилируется 'Здесь мы пытаемся создать объект Person из другого объекта Person, что представляется очевидным случаем копирующего конструирования (р является lvalue, так чтоможно выбросить из головы все фантазии на тему копирования с помощью операции перемещения).
Но этот код не вызывает копирующий конструктор - он вызывает конструктор с прямой передачей. Затем эта функция будет пытаться инициализировать член-данные s t d : : s t ring объекта Person значением из объекта Person (р}. Классstd : : st ring не имеет конструктора, получающего параметр типа Person, так что вашкомпилятор будет вынужден просто развести руками и наказать вас длинными и непонятными сообщениями об ошибках."Но почему, - можете удивиться вы, - вызывается конструктор с прямой передачей, а не копирующий конструктор? Мы же инициализируем Person другим объектомPerson!" Да, это так, но компиляторы приносят присягу свято соблюдать правила С++,а правила, имеющие отношение к данной ситуации, - это правила разрешения вызововперегруженных функций.С(S.
4.J)И збегайте перегрузок для универсальных ссылок187Компиляторы рассуждают следующим образом. cloneOf P инициализируется неконстантным lvalue (р}, а это означает, что шаблонный конструктор может быть инстанцирован для получения неконстантного lvalue типа Person. После такого инстанцированиякласс Person выглядит следующим образом:class Person {puЬl ic :explicit Person (Person& n)/ / Инстанцирован из: name ( std : : forward<Persoп&> ( n ) ) { } / / шаблона с прямой1 1 передачейexpl icit Person ( int idx ) ;Persoп ( const Person& rhs ) ;)/ / Как и ранее/ / Копирующий конструктор11 ( сгенерирован компилятором);В инструкцииauto cloneOfP ( p ) ;рможет быть передан либо копирующему конструктору, либо инстанцированному шаблону. Вызов копирующего конструктора для точного соответствия типа параметра требует добавления к р модификатора const; вызов инстанцированного шаблона никакихдобавлений не требует.
Таким образом, перегрузка, сгенерированная из шаблона, представляет собой лучшее соответствие, так что компиляторы делают то, для чего предназначены: генерируют вызов той функции, которая соответствует наилучшим образом."Копирование" неконстантных lvalue типа Person, таким образом, осуществляется конструктором с прямой передачей, а не копирующим конструктором.Если мы немного изменим пример, так, чтобы копируемый объект был константным,то увидим совершенно иную картину:const Person cp ( "Nancy" ) ; / / Теперь объект константныйauto cloneOfP ( ер ) ;// Вызов копирующего конструктора !Поскольку копируемый объект теперь объявлен как const, он в точности соответствуеттипу параметра, получаемого копирующим конструктором.
Шаблонизированный конструктор также может быть инстанцирован таким образом, чтобы иметь ту же сигнатуру:class PersonpuЫic :explicit Person (const Person& n) ; / / Инстанцирован из шаблонаPerson ( const Person& rhs ) ;/ / Копирующий конструктор1 1 ( сгенерирован компилятором)};Но это не имеет значения, поскольку одно из правил разрешения перегрузок в С++гласит, что в ситуации, когда инстанцирование шаблона и нешаблонная функция (т.е."нормальная" функция) имеют одинаково хорошее соответствие, предпочтение отдается1 88Глава S. Rvаluе-ссылки, семантика перемещений и прямая передачанормальной функции.
Поэтому все козыри оказываются на руках копирующего конструктора (нормальной функции) с той же самой сигнатурой.(Если вам интересно, почему компиляторы создают копирующий конструктор, еслиони могут инстанцировать шаблонный конструктор с той же сигнатурой, обратитеськ разделу 3. 1 1 .)Взаимодействие между конструкторами с прямой передачей и сгенерированнымикомпилятором операциями копирования и перемещения становится еще более сложным,когда в картину включается наследование.
В частности, обычные реализации копирующих и перемещающих операций производного класса ведут себя совершенно неожиданно. Взгляните на следующий код:class Spec i alPerson : puЫic PersonpuЫ ic:SpecialPerson ( const Specia lPerson& rhs ) / / КопирующийPerson (rhs)11 конструктор ; вызывает конструктор11 базового класса с прямой передачей !{ ". )SpecialPerson ( SpecialPerson&& rhs )// ПеремещающийPerson (std: : move (rhs) ) / / конструктор ; вызывает конструктор/ / базового класса с прямой передачей 1{ . )");Как указывают комментарии, копирующий и перемещающий конструкторы производного класса не вызывают копирующий и перемещающий конструкторы базового класса; они вызывают конструктор базового класса с прямой передачей! Чтобы понять, почему, обратите внимание, что функции производного класса используют аргументы типаSpec i a l Person для передачи в базовый класс, после чего в игру вступает разрешение перегрузок для конструкторов в классе Person.
В конечном итоге код не будет компилироваться, потому что у std : : st ring нет никакого конструктора, принимающего Special Persoп.Я надеюсь, что теперь я убедил вас, что перегрузка для параметров, являющихся универсальными ссылками, - это то, чего лучше избегать, насколько это возможно. Но если перегрузка для универсальной ссылки - плохая идея, то что же делать, если вам нужна функция,которая выполняет передачу большинства типов аргументов, но при этом должна обрабатывать некоторые из них особым образом? Это яйцо может быть разбито массой способов.Этих способов так много, что я посвятил им целый раздел - раздел 5.5, который идет сразупосле того, который вы сейчас читаете.
Не останавливайтесь, и вы попадете прямо в него.Спедует запомнить•Перегрузка для универсальных ссылок почти всегда приводит к тому, что даннаяперегрузка вызывается чаще, чем вы ожидаете.•Особенно проблематичны конструкторы с прямой передачей, поскольку они обычносоответствуют неконстантным lvalue лучше, чем копирующие конструкторы, и моrутперехватывать вызовы из производного класса копирующих и перемещающих конструкторов базового класса.S.4. Избегайте перегрузок дnя универсальных ссыпок1 89S .S . Знакомство с а nыернативами пере r рузкидnя универсаnьных ссыпокВ разделе 5.4 поясняется, что перегрузка для универсальных ссылок может привести к целому ряду проблем как для автономных функций, так и для функций-членов (вособенности для конструкторов).
Тем не менее в нем также приводятся примеры, когдатакая перегрузка может оказаться полезной, если только она будет вести себя так, какмы· хотим! В этом разделе исследуются способы достижения желаемого поведения либопутем проектирования, позволяющего избежать перегрузок для универсальных ссылок,либо путем применения их таким образом, чтобы ограничить типы аргументов, которымони могут соответствовать.Ниже использованы примеры, представленные в разделе 5.4.