Как я могу эффективно выбрать контейнер стандартной библиотеки в С++ 11?

Есть известное изображение (шпаргалка) под названием "Выбор контейнера С++". Это блок-схема выбора наилучшего контейнера для желаемого использования.

Кто-нибудь знает, есть ли уже его версия для С++ 11?

Это предыдущий: Выбор контейнера eC++


person BlakBat    schedule 22.05.2012    source источник
comment
Никогда не видел этого раньше. Благодарность!   -  person WeaselFox    schedule 22.05.2012
comment
@WeaselFox: это уже часть C++-Faq здесь, на SO.   -  person Alok Save    schedule 22.05.2012
comment
В C++11 представлен только один новый настоящий тип контейнера: контейнеры unordered_X. Их включение только значительно запутало бы таблицу, поскольку при принятии решения о целесообразности использования хеш-таблицы необходимо учитывать ряд соображений.   -  person Nicol Bolas    schedule 22.05.2012
comment
@NicolBolas: Также forward_list и array. И неупорядоченные контейнеры на самом деле не запутали бы его; они просто добавят новый кластер, связанный с Need to find element by key.   -  person Mike Seymour    schedule 22.05.2012
comment
@MikeSeymour: неупорядоченные контейнеры будут очень большим кластером; вот почему это запутало бы таблицу. Решение о том, использовать хеш-таблицу или нет, — это не простая пара решений.   -  person Nicol Bolas    schedule 22.05.2012
comment
@NicolBolas Но это уже верно для старых контейнеров. Я часто использую std::vector для упорядоченных контейнеров, сохраняя сортировку данных самостоятельно.   -  person James Kanze    schedule 22.05.2012
comment
Джеймс прав, случаев использования вектора больше, чем показано в таблице. Преимущество локальности данных во многих случаях превосходит недостаток эффективности в некоторых операциях (скоро C++11). Я не нахожу электронную диаграмму такой полезной даже для С++ 03   -  person David Rodríguez - dribeas    schedule 22.05.2012
comment
Это мило, но я думаю, что чтение любого обычного учебника по структурам данных оставит вас в состоянии, когда вы сможете не только заново изобрести эту блок-схему за несколько минут, но и узнать гораздо больше полезных вещей, которые эта блок-схема замалчивает.   -  person Andrew Tomazos    schedule 22.05.2012
comment
В любом случае, есть ли обновленная версия этой блок-схемы? Это простой и очень хороший планировщик.   -  person mr5    schedule 18.06.2013
comment
Порядок важен? -› Сортировать вектор. Хранить ключ отдельно от значения? -› Использовать вектор‹ кортеж‹ключ, значение›› и упорядочить по ключу.   -  person user1911091    schedule 14.08.2019


Ответы (4)


Не то, чтобы я знал, однако это можно сделать в текстовом виде, я думаю. Кроме того, диаграмма немного неверна, потому что list в целом не такой уж хороший контейнер, как и forward_list. Оба списка представляют собой очень специализированные контейнеры для нишевых приложений.

Чтобы построить такую ​​диаграмму, вам просто нужны две простые рекомендации:

  • Сначала выберите семантику
  • Когда есть несколько вариантов, выбирайте самый простой

Беспокоиться о производительности поначалу обычно бесполезно. Соображения большого O действительно начинают действовать только тогда, когда вы начинаете обрабатывать несколько тысяч (или более) элементов.

Есть две большие категории контейнеров:

  • Ассоциативные контейнеры: у них есть операция find
  • Контейнеры Simple Sequence

а потом поверх них можно построить несколько адаптеров: stack, queue, priority_queue. Я оставлю здесь адаптеры, они достаточно специализированы, чтобы быть узнаваемыми.


Вопрос 1: Ассоциативный?

  • Если вам нужно легко искать по одному ключу, вам нужен ассоциативный контейнер
  • Если вам нужно отсортировать элементы, вам нужен упорядоченный ассоциативный контейнер
  • В противном случае переходите к вопросу 2.

Вопрос 1.1: Заказано?

  • Если вам не нужен определенный порядок, используйте контейнер unordered_, в противном случае используйте его традиционный упорядоченный аналог.

Вопрос 1.2: Отдельный ключ?

  • Если ключ отделен от значения, используйте map, в противном случае используйте set.

Вопрос 1.3: Дубликаты?

  • Если вы хотите сохранить дубликаты, используйте multi, иначе не делайте этого.

Пример:

Предположим, что у меня есть несколько человек с уникальным идентификатором, связанным с ними, и я хотел бы получить данные о человеке из его идентификатора как можно проще.

  1. Мне нужна функция find, то есть ассоциативный контейнер

1.1. Мне наплевать на порядок, поэтому контейнер unordered_

1.2. Мой ключ (ID) отделен от значения, с которым он связан, поэтому map

1.3. Идентификатор уникален, поэтому не должно быть дубликатов.

Окончательный ответ: std::unordered_map<ID, PersonData>.


Вопрос 2: Стабильная память?

  • Если элементы должны быть стабильными в памяти (т. е. они не должны перемещаться при изменении самого контейнера), то используйте некоторые list
  • В противном случае переходите к вопросу 3.

Вопрос 2.1: Какой?

  • Соглашайтесь на list; forward_list полезен только для меньшего объема памяти.

Вопрос 3: Динамический размер?

  • Если контейнер имеет известный размер (во время компиляции), и этот размер не будет изменен в ходе работы программы, и элементы являются конструируемыми по умолчанию или вы можете предоставить полный список инициализации (используя синтаксис { ... }), а затем использовать array. Он заменяет традиционный C-массив, но с удобными функциями.
  • В противном случае переходите к вопросу 4.

Вопрос 4: Двусторонний?

  • Если вы хотите иметь возможность удалять элементы как спереди, так и сзади, используйте deque, в противном случае используйте vector.

Вы заметите, что по умолчанию, если вам не нужен ассоциативный контейнер, ваш выбор будет vector. Оказывается, это также рекомендации Саттера и Страуструпа.

person Matthieu M.    schedule 22.05.2012
comment
+1, но с некоторыми примечаниями: 1) array не требует конструктивного типа по умолчанию; 2) выбор multi заключается не столько в том, что дубликаты разрешены, сколько в том, имеет ли значение их сохранение (вы можете поместить дубликаты в контейнеры, отличные от multi, просто бывает, что только один сохраняется). - person R. Martinho Fernandes; 22.05.2012
comment
Пример немного не тот. 1) мы можем найти (не функцию-член, а «алгоритм») в неассоциативном контейнере, 1.1), если нам нужно найти эффективно, и unordered_ будет O (1), а не O (log n). - person BlakBat; 22.05.2012
comment
@BlakBat: map.find(key) гораздо приятнее, чем std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));, поэтому семантически важно, чтобы find была функцией-членом, а не функцией из <algorithm>. Что касается O(1) и O(log n), это не влияет на семантику; Я уберу эффективно из примера и заменю его на легко. - person Matthieu M.; 22.05.2012
comment
Если элементы должны быть стабильными в памяти... тогда используйте какой-нибудь список... хммм, я думал, что deque тоже имеет это свойство? - person Martin Ba; 05.07.2013
comment
@MartinBa: Да и нет. В deque элементы стабильны только, если вы толкаете/отталкиваете с любого конца; если вы начинаете вставлять/стирать в середине, то до N/2 элементов перемешиваются, чтобы заполнить созданный пробел. - person Matthieu M.; 05.07.2013

Мне нравится ответ Матье, но я собираюсь переформулировать блок-схему следующим образом:

Когда НЕ использовать std::vector

По умолчанию, если вам нужен контейнер с вещами, используйте std::vector. Таким образом, любой другой контейнер оправдан только предоставлением некоторой функциональности, альтернативной std::vector.

Конструкторы

std::vector требует, чтобы его содержимое можно было перемещать, так как он должен иметь возможность перетасовывать элементы. Это не страшное бремя для содержимого (обратите внимание, что конструкторы по умолчанию не требуются, благодаря emplace и т. д.). Однако большинству других контейнеров не требуется какой-либо конкретный конструктор (опять же, благодаря emplace). Поэтому, если у вас есть объект, в котором вы абсолютно не можете реализовать конструктор перемещения, вам придется выбрать что-то другое.

std::deque будет общей заменой, имеющей многие свойства std::vector, но вы можете вставлять только на обоих концах двухсторонней очереди. Вставки посередине требуют перемещения. std::list не предъявляет никаких требований к своему содержимому.

Нужны логические значения

std::vector<bool> это... нет. Ну это стандартно. Но это не vector в обычном смысле, так как операции, которые std::vector обычно разрешены, запрещены. И уж точно не содержит bools.

Следовательно, если вам нужно настоящее vector поведение из контейнера bool, вы не получите его от std::vector<bool>. Так что вам придется заплатить std::deque<bool>.

Searching

Если вам нужно найти элементы в контейнере, а тег поиска не может быть просто индексом, то вам может понадобиться отказаться от std::vector в пользу set и map. Обратите внимание на ключевое слово "может"; отсортированный std::vector иногда является разумной альтернативой. Или flat_set/map, который реализует отсортированный std::vector.

В настоящее время существует четыре их варианта, каждый со своими потребностями.

  • Используйте map, если тег поиска не совпадает с самим элементом, который вы ищете. В противном случае используйте set.
  • Используйте unordered, если у вас много элементов в контейнере и производительность поиска обязательно должна быть O(1), а не O(logn).
  • Используйте multi, если вам нужно, чтобы несколько элементов имели один и тот же тег поиска.

Заказ

Если вам нужно, чтобы контейнер элементов всегда сортировался на основе конкретной операции сравнения, вы можете использовать set. Или multi_set, если вам нужно, чтобы несколько элементов имели одинаковое значение.

Или вы можете использовать отсортированный std::vector, но вам придется держать его в сортированном виде.

Стабильность

Когда итераторы и ссылки становятся недействительными, иногда возникает проблема. Если вам нужен список элементов, так что у вас есть итераторы/указатели на эти элементы в других местах, то подход std::vector к аннулированию может быть неподходящим. Любая операция вставки может привести к аннулированию в зависимости от текущего размера и емкости.

std::list предлагает твердую гарантию: итератор и связанные с ним ссылки/указатели становятся недействительными только тогда, когда сам элемент удаляется из контейнера. std::forward_list есть, если память вызывает серьезную озабоченность.

Если это слишком сильная гарантия, std::deque предлагает более слабую, но полезную гарантию. Аннулирование происходит из-за вставок в середине, но вставки в начало или конец приводят к аннулированию только итераторов, а не указателей/ссылок на элементы в контейнере.

Производительность вставки

std::vector обеспечивает дешевую вставку только в конце (да и то становится дорого, если продуть емкость).

std::list дорого с точки зрения производительности (каждый новый вставленный элемент требует выделения памяти), но он непротиворечив. Он также предлагает иногда незаменимую возможность перетасовывать предметы практически без затрат на производительность, а также обменивать предметы с другими std::list контейнерами того же типа без потери производительности. Если вам нужно много перетасовать вещи, используйте std::list.

std::deque обеспечивает вставку/удаление с постоянным временем в начале и в конце, но вставка в середине может быть довольно дорогой. Так что, если вам нужно добавить/удалить что-то как спереди, так и сзади, std::deque может быть тем, что вам нужно.

Следует отметить, что благодаря семантике перемещения производительность вставки std::vector может быть не такой плохой, как раньше. В некоторых реализациях была реализована форма копирования элемента на основе семантики перемещения (так называемая «обмен подкачками»), но теперь, когда перемещение является частью языка, оно предписано стандартом.

Нет динамических распределений

std::array — прекрасный контейнер, если вам нужно как можно меньше динамических выделений. Это просто оболочка вокруг C-массива; это означает, что его размер должен быть известен во время во время компиляции. Если вы можете жить с этим, используйте std::array.

При этом использование std::vector и reserve размера будет работать так же хорошо для ограниченного std::vector. Таким образом, фактический размер может варьироваться, и вы получаете только одно выделение памяти (если только вы не убьете емкость).

person Nicol Bolas    schedule 23.05.2012
comment
Что ж, мне тоже очень нравится ваш ответ :) WRT сохраняет отсортированный вектор, кроме std::sort, есть еще std::inplace_merge, который интересно легко размещать новые элементы (а не вызов std::lower_bound + std::vector::insert). Приятно узнать о flat_set и flat_map! - person Matthieu M.; 23.05.2012
comment
Вы также не можете использовать вектор с 16-байтовыми выровненными типами. Также хорошей заменой vector<bool> является vector<char>. - person Inverse; 26.05.2012
comment
@Inverse: вы также не можете использовать вектор с типами, выровненными по 16 байтам. Говорит кто? Если std::allocator<T> не поддерживает это выравнивание (и я не знаю, почему бы и нет), вы всегда можете использовать свой собственный распределитель. - person Nicol Bolas; 26.05.2012
comment
@Inverse: C++11 std::vector::resize имеет перегрузку, которая не принять значение (он просто принимает новый размер; любые новые элементы будут создаваться по умолчанию на месте). Кроме того, почему компиляторы не могут правильно выровнять параметры значений, даже если они объявлены как имеющие такое выравнивание? - person Nicol Bolas; 27.05.2012
comment
@NicolBolas: бьет меня! Я рад, что С++ 11 исправил интерфейс. - person Inverse; 28.05.2012
comment
bitset для bool, если вы заранее знаете размер en.cppreference.com/w/cpp/ утилита/битсет - person bendervader; 02.07.2015
comment
@bendervader bitset ничего не делает для устранения предупреждений этого сообщения о vector<bool>, учитывая, что у него те же проблемы: на самом деле он не содержит набор экземпляров bool, а скорее упакованные биты, доступ к которым осуществляется через экземпляры прокси-класса, возвращаемые по значению. В контексте этого вопроса его единственная сила по сравнению с vector<bool> заключается в том, что он не обладает безнадежно вводящим в заблуждение именем и бесконечным презрением со стороны всех, кто связан с языком. Кстати, вместо того, чтобы сдаться и использовать deque для получения реального контейнера bool, мы можем довольно легко создать тонкую структуру-оболочку вокруг экземпляра bool. - person underscore_d; 11.09.2016

Вот версия C++11 приведенной выше блок-схемы. [первоначально опубликовано без указания авторства, Mikael Persson]

person Wasim Thabraze    schedule 22.06.2014
comment
@NO_NAME Ух ты, я рад, что кто-то удосужился указать источник. - person underscore_d; 11.09.2016

Вот быстрое вращение, хотя, вероятно, оно нуждается в доработке

Should the container let you manage the order of the elements?
Yes:
  Will the container contain always exactly the same number of elements? 
  Yes:
    Does the container need a fast move operator?
    Yes: std::vector
    No: std::array
  No:
    Do you absolutely need stable iterators? (be certain!)
    Yes: boost::stable_vector (as a last case fallback, std::list)
    No: 
      Do inserts happen only at the ends?
      Yes: std::deque
      No: std::vector
No: 
  Are keys associated with Values?
  Yes:
    Do the keys need to be sorted?
    Yes: 
      Are there more than one value per key?
      Yes: boost::flat_map (as a last case fallback, std::map)
      No: boost::flat_multimap (as a last case fallback, std::map)
    No:
      Are there more than one value per key?
      Yes: std::unordered_multimap
      No: std::unordered_map
  No:
    Are elements read then removed in a certain order?
    Yes:
      Order is:
      Ordered by element: std::priority_queue
      First in First out: std::queue
      First in Last out: std::stack
      Other: Custom based on std::vector????? 
    No:
      Should the elements be sorted by value?
      Yes: boost::flat_set
      No: std::vector

Вы можете заметить, что это сильно отличается от версии C++03, прежде всего из-за того, что я очень не люблю связанные узлы. Контейнеры связанных узлов обычно могут быть лучше несвязанных контейнеров по производительности, за исключением нескольких редких ситуаций. Если вы не знаете, что это за ситуации, и у вас есть доступ к ускорению, не используйте контейнеры связанных узлов. (std::list, std::slist, std::map, std::multimap, std::set, std::multiset). Этот список в основном сосредоточен на малых и средних контейнерах, потому что (А) это 99,99% того, с чем мы имеем дело в коде, и (Б) для большого количества элементов нужны специальные алгоритмы, а не разные контейнеры.

person Mooing Duck    schedule 16.05.2014