С. Мейерс - Эффективный и современный C++ (1114942), страница 72
Текст из файла (страница 72)
Этоотличие позволяет функциям размещения избегать создания и уничтожения временныхобъектов, которые могут требоваться функциям вставки.Поскольку функции размещения может быть передан аргумент типа, хранимогов контейнере (аргумент, таким образом, заставляет функцию выполнить копирующееили перемещающее конструирование), размещение может использоваться даже тогда,когда функция вставки не требует временного объекта. В таком случае вставка и размещение делают, по сути, одно и то же. Например, дляstd: : str i n g queenOfDisco ( " Donna Summe r " ) ;оба приведенных далее вызова корректны, и оба приводят к одному и тому же результату:vs .
push_Ьack ( queenOfDisco ) ;11 Копирующее конструирование//vs . emplace_Ьack ( queenOfDisco ) ;queenOfDiscoв концеvs11 То же самоеТаким образом, размещающие функции могут делать все то же самое, что и функциивставки. И ногда они делают это более эффективно и как минимум теоретически недолжны делать менее эффективно. Так почему же мы не используем их все время?8.2. Рассмотрите п рименение размещения вместо вставки293Потому что теоретически разницы между теорией и практикой нет, а практически есть. При текущих реализациях стандартной библиотеки имеются ситуации, когда, каки ожидается, размещение превосходит вставку, но - увы! - есть ситуации, когда вставка работает быстрее. Такие ситуации непросто охарактеризовать, поскольку они зависятот типов передаваемых аргументов, используемых контейнеров, местоположения вставкиили размещения в контейнере, безопасности исключений конструкторов типов, содержащихся в контейнере, и для контейнеров, в которых запрещены дубликаты (т.е.
std : : set,std : : map, std : : unordered_ set, std : : unordered_map) , от того, содержится ли уже в контейнере вставляемое значение. А потому следует пользоваться обычным советом по повышению производительности: для определения того, какой метод работает быстрее,надо сравнивать их реальную производительность в конкретных условиях.Это, конечно, не очень приятно, так что вы будете рады узнать, что есть эвристический алгоритм, который может помочь вам определить ситуации, когда, скорее всего,имеет смысл использовать функции размещения.
Если все приведенные далее утверждения справедливы, размещение почти наверняка будет опережать вставку.•Пример, с которого начат данный раздел (добавление s t d : : s t r i ng со значением"x y z z y " в std : : vector vs), демонстрирует значение, добавляемое в конец вектораvsв место, где пока что нет никакого объекта. Таким образом, новое значениедолжно быть сконструировано в std : : vector. Если мы пересмотрим пример так,что новая строка std : : st ring будет направляться в местоположение, уже занятоеобъектом, это будет совсем другая история:Добавляемое значение конструируется в контейнере, а не присваивается.-std: : vector<std : : string> vs ;11 Как и ранее11 Добавление элементов в vsvs .
emplace (vs . Ьegin ( ) , "xyzzy" ) ; // Добавление "xyzzy" в начало vsПри таком коде только редкие реализации будут конструировать добавляемыйобъект std : : st ring в памяти, занятой vs [ О ] . Большинство реализаций используютперемещающее присваивание в указанное место. Но перемещающее присваиваниетребует наличия перемещаемого объекта, а это означает, что необходимо созданиевременного объекта. Поскольку основное преимущество размещения над вставкойзаключается в том, что не создаются и не уничтожаются временные объекты,при добавлении значения в контейнер с помощью присваивания преимуществаразмещения исчезают.Увы, выполняется ли добавление значения в контейнер путем конструирования илиперемещения, в общем случае зависит от реализации.
И вновь на помощь можетприйти эвристика.Контейнеры на основе узлов почти всегда используют для добавления новых значенийконструирование, а большинство стандартных контейнеров я вляются именнотаковыми. Исключениями являются s t d : : vect or, s t d : : de que и s t d : : s t r i n g .(Контейнер s t d : : a r r a y также не является таковым, но он не поддерживаетн и вставку, ни размещение, поэтому упоми нать о нем здесь нет смысла.) Вконтейнерах, не основанных на узлах, можно рассчитывать на использование294Глава 8. Тонкостифункцией emplace back для размещения нового значения конструирования вместоприсваивания; то же самое можно сказать и о функции emplace_ f ront контейнераstd : : deque.•Типы передаваемых аргументов отличаются от типа, хранящеrося в контейнере.И вновь, преимущество размещения по сравнению со вставкой в общем случае связано с тем фактом, что его интерфейс не требует создания и уничтожения временного объекта при передаче аргументов типа, отличного от типа, хранящегося в контейнере.
Когда в контейнер c on t a i n e r<T> добавляется объект типа Т, нет причиножидать, что размещение окажется быстрее вставки, поскольку для удовлетворенияинтерфейсу вставки не требуется создание временного объекта.•Маловероятно, что контейнер отверrнет новое значение как дубликат. Это означает, что либо контейнер разрешает наличие дубликатов, либо большинство передаваемых значений уникальны. Это важно, поскольку для того, чтобы определить наличие дубликата, реализации размещения обычно создают узел с новым значением,а затем сравнивают его с имеющимися узлами контейнера. Если добавляемое значение в контейнере отсутствует, узел встраивается в контейнер.
Однако, если такоезначение уже есть в контейнере, размещение прерывается, а узел уничтожается, такчто впустую расходуется стоимость создания и уничтожения объекта. Такие узлыдля функций размещения создаются более часто, чем для функций вставки.Приведенные вызовы, с которыми мы уже сталкивались в данном разделе, удовлетворяют всем перечисленным критериям. Они работают быстрее соответствующих вызововpush_back.vs . emplace_back ( "xyzzy" ) ; //////11vs . emplace_back ( SO , ' х ' ) ; 11Конструирует новое значение в концеконтейнера ; тип аргумента отличен оттипа, хранимого в контейнере ;контейнер не отвергает дубликатыТо же самоеПри принятии решения об использовании функций размещения стоит иметь в видуеще пару вопросов.
Первый из них связан с управлением ресурсами. Предположим, чтоу вас есть контейнер с объектами s t d : : shared_pt r <Widget>std: : list<std : : shared_ptr<Widget>> ptr s ;и в ы хотите добавить s t d : : sha red_ptr, который должен быть освобожден с помощьюпользовательского удалителя (см. раздел 4.2). В разделе 4.4 поясняется, что по возможности вы должны использовать для создания std : : shared _ptr функцию s t d : : make_shared,но в нем же указано, что существуют ситуации, когда это невозможно. Одна из такихситуаций - когда вы хотите указать пользовательский удалитель.
В этом случае для получения обычного указателя, которым будет управлять интеллектуальный указательstd : : shared_pt r, вы должны непосредственно использовать оператор new.Если пользовательский удалитель представляет собой функциюvoid kil lWidget (Widget * pWidget ) ;8.2. Рассмотрите п рименение размещения вместо вставки295то код, использующий фун кцию вставки, может выглядеть следующим образом:ptrs .push_Ьack ( std : : shared_ptr<Widget> ( new Widge t , kil lWidge t ) ) ;Он может также принять и такой вид, означающий то же самое:ptrs .push_Ьack ( { new Widge t , killWidget } ) ;В любом случае перед вызовом p u s h _b a c k будет создан времен ный объектstd : : sha red_pt r.
Параметром push_back является ссылка на s t d : : shared_pt r, так чтодолжен быть объект, на который ссылается этот параметр.Фун кция e m p l a c e _ b a c k должна избегать создания временного объектаs t d : : s hared_ptr, но в этом случае ценность временного объекта гораздо выше его стоимости. Рассмотрим следующую потенциальную последовательность событий.1. В любом из приведенных выше вызовов конструируется временный объектstd : : shared_pt r<Widget >, хранящий простой указатель, являющийся результатом операции "new Widget': Назовем этот объект t emp.2. Функция push_b a c k получает t emp по ссылке. В процессе выделения памятидля узла списка, который должен содержать копию t emp, генерируется исключение нехватки памяти.3. При выходе исключения за пределы push_back объект t emp уничтожается. Поскольку это единственный интеллектуальный указатель std : : sha red_pt r, указывающий на управляемый им объект W idget, он автоматически удаляет последний,в данном случае с помощью вызова ki l lWidget.Несмотря на происшедшую генерацию исключения нет никаких утечек: Widget, созданный с помощью " new Widget" в вызове push_back, освобождается деструктором объекта std : : sha red_pt r, который был создан для управления им (объектом t emp).
Все отлично.Рассмотрим теперь, что произойдет при вызове emplace _back вместо push_back:ptrs . emplace_Ьack ( new Widge t , killWidget ) ;1 . Обычный указатель, являющийся результатом выполнения "new Widget", передается с помощью прямой передачи в точку внутри emp l ace back, где выделяетсяпамять для узла списка. При попытке выделения памяти генерируется исключениенехватки памяти._2. При выходе исключения за пределы emp l a ce _back обычный указатель, которыйбыл единственным средством доступа к W idget в динамической памяти, оказывается потерянным. Происходит утечка W idget (и всех ресурсов, которыми владеетэтот объект).В этом сценарии все совсем не отлично, и класс s t d : : s ha red_pt r в этом не повинен.
Та же самая проблема возникнет при использовании s t d : : u n i que_pt r с пользовательским удалителем. По существу, эффективность классов управления ресурсами,296Гл ава 8. Тонкоститаких как s t d : : shared_ptr и std : : un i que_ptr, основана на немедленной передаче ресурсов (таких, как обычные указатели , возвращаемые оператором new) конструкторамуправляющих ресурсами объектов. Тот факт, что функции наподобие std : : make sharedи std : : make_unique автоматизируют этот процесс, является одной из причин, по которым эти функции так важны.В вызовах функций вставки контейнеров, хранящих управляющие ресурсами объекты (например, s t d : : l ist<std : : shared_pt r<Widget»), типы параметров функций в общем случае гарантируют, что между захватом ресурса (например, использованием оператора new) и конструированием управляющего ресурсом объекта ничего не происходит.В функциях размещения прямая передача откладывает создание управляющих ресурсами объектов до тех пор, пока они не смогут быть сконструированы в памяти контейнера,и тем самым открывают окно, генерация исключения в котором может привести к утечкересурсов.
Все стандартные контейнеры подвержены этой проблеме. При работе с контейнерами, хранящими управляющие ресурсами объекты, вы должны принять меры,гарантирующие, что при выборе функции размещения вместо функции вставки вы незаплатите за повышение эффективности безопасностью исключений.Откровенно говоря, вы в любом случае не должны передавать выражения наподобие"new Widget" в функции emplace_back и push_back, как и в большинство любых другихфункций, поскольку, как поясняется в разделе 4.4, это ведет к возможным проблемамс безопасностью исключений, одну из которых мы только что рассмотрели. Для предотвращения неприятностей требуется, чтобы получение указателя от "new Widget" и превращение его в управляющий ресурсом объект выполнялось в одной инструкции, а ужезатем этот объект передавался как rvalue функции, которой вы хотели изначально передавать "new Widget': (Более детально этот подход рассматривается в разделе 4.4.) Такимобразом, код, использующий push_back, должен быть записан скорее какstd: : sha.redytr<Widqet> spw (new Widqet, / / Создание Widget иkillWidqet) ; / / передача его spwpt r s .