web-dev-qa-db-fra.com

Comment puis-je sélectionner efficacement un conteneur de bibliothèque standard en C ++ 11?

Il existe une image bien connue (aide-mémoire) appelée "Choix du conteneur C++". C'est un organigramme pour choisir le meilleur conteneur pour l'utilisation souhaitée.

Quelqu'un sait-il s'il existe déjà une version C++ 11?

Voici le précédent: eC++ Container choice

130
BlakBat

Pas que je sache, mais cela peut être fait textuellement, je suppose. En outre, le graphique est légèrement désactivé, car list n'est pas un bon conteneur en général, et forward_list. Les deux listes sont des conteneurs très spécialisés pour des applications de niche.

Pour construire un tel graphique, vous avez juste besoin de deux directives simples:

  • Choisissez d'abord la sémantique
  • Lorsque plusieurs choix sont disponibles, optez pour le plus simple

S'inquiéter des performances est généralement inutile au début. Les grandes considérations O n'interviennent vraiment que lorsque vous commencez à manipuler quelques milliers (ou plus) d'articles.

Il existe deux grandes catégories de conteneurs:

  • Conteneurs associatifs : ils ont une opération find
  • Conteneurs à séquence simple

puis vous pouvez créer plusieurs adaptateurs par-dessus: stack, queue, priority_queue. Je vais laisser les adaptateurs ici, ils sont suffisamment spécialisés pour être reconnaissables.


Question 1: Associative ?

  • Si vous avez besoin de rechercher facilement par une clé , alors vous avez besoin d'un conteneur associatif
  • Si vous avez besoin de trier les éléments, vous avez besoin d'un conteneur associatif ordonné
  • Sinon, passez à la question 2.

Question 1.1: Commandé ?

  • Si vous n'avez pas besoin d'une commande spécifique, utilisez un unordered_ conteneur, sinon utilisez son homologue ordonné traditionnel.

Question 1.2: Clé séparée ?

  • Si la clé est distincte de la valeur, utilisez un map, sinon utilisez un set

Question 1.3: Doublons ?

  • Si vous souhaitez conserver les doublons, utilisez un multi, sinon ne le faites pas.

Exemple:

Supposons que j'ai plusieurs personnes avec un ID unique qui leur est associé et que je souhaite récupérer les données d'une personne à partir de son ID aussi simplement que possible.

  1. Je veux une fonction find, donc un conteneur associatif

    1.1. Je me fiche de l'ordre, donc un unordered_ récipient

    1.2. Ma clé (ID) est distincte de la valeur à laquelle elle est associée, donc un map

    1.3. L'ID est unique, donc aucun doublon ne doit s'introduire.

La réponse finale est: std::unordered_map<ID, PersonData>.


Question 2: Mémoire stable ?

  • Si les éléments doivent être stables en mémoire (c'est-à-dire qu'ils ne doivent pas se déplacer lorsque le conteneur lui-même est modifié), alors utilisez un list
  • Sinon, passez à la question 3.

Question 2.1: Lequel ?

  • Se contenter d'un list; une forward_list n'est utile que pour une empreinte mémoire moindre.

Question 3: Taille dynamique ?

  • Si le conteneur a une taille connue (au moment de la compilation), et cette taille ne sera pas modifiée au cours du programme, et les éléments sont par défaut constructible ou vous pouvez fournir une liste d'initialisation complète (en utilisant le { ... } syntaxe), puis utilisez un array. Il remplace le tableau C traditionnel, mais avec des fonctions pratiques.
  • Sinon, passez à la question 4.

Question 4: Double ?

  • Si vous souhaitez pouvoir supprimer des éléments du recto et du verso, utilisez un deque, sinon utilisez un vector.

Vous remarquerez que, par défaut, sauf si vous avez besoin d'un conteneur associatif, votre choix sera un vector. Il s'avère que c'est aussi recommandation de Sutter et Stroustrup .

93
Matthieu M.

J'aime la réponse de Matthieu, mais je vais reformuler l'organigramme comme suit:

Quand ne PAS utiliser std :: vector

Par défaut, si vous avez besoin d'un conteneur de trucs, utilisez std::vector. Ainsi, tout autre conteneur n'est justifié qu'en fournissant une alternative fonctionnelle à std::vector.

Constructeurs

std::vector Nécessite que son contenu soit constructible, car il doit pouvoir mélanger les éléments autour. Ce n'est pas un lourd fardeau à placer sur le contenu (notez que les constructeurs par défaut ne sont pas pas nécessaires , grâce à emplace et ainsi de suite) . Cependant, la plupart des autres conteneurs ne nécessitent aucun constructeur particulier (encore une fois, grâce à emplace). Donc, si vous avez un objet où vous ne pouvez absolument pas implémenter un constructeur de déplacement, alors vous devrez choisir autre chose.

Un std::deque Serait le remplacement général, ayant de nombreuses propriétés de std::vector, Mais vous ne pouvez insérer que les deux extrémités du deque. Les inserts au milieu nécessitent un déplacement. Un std::list N'impose aucune exigence sur son contenu.

Besoins Bools

std::vector<bool> N'est ... pas. Eh bien, c'est standard. Mais ce n'est pas un vector au sens habituel, car les opérations que std::vector Permet normalement sont interdites. Et très certainement ne contient pas bools .

Par conséquent, si vous avez besoin d'un comportement réel de vector à partir d'un conteneur de bools, vous ne l'obtiendrez pas de std::vector<bool>. Vous devrez donc vous acquitter d'un std::deque<bool>.

Recherche

Si vous devez trouver des éléments dans un conteneur et que la balise de recherche ne peut pas être simplement un index, vous devrez peut-être abandonner std::vector Au profit de set et map . Notez le mot clé " may "; un std::vector trié est parfois une alternative raisonnable. Ou Boost.Container flat_set/map , qui implémente un std::vector Trié.

Il en existe maintenant quatre variantes, chacune ayant ses propres besoins.

  • Utilisez un map lorsque la balise de recherche n'est pas la même chose que l'élément que vous recherchez lui-même. Sinon, utilisez un set.
  • Utilisez unordered lorsque vous avez beaucoup d'éléments dans le conteneur et que les performances de recherche doivent absolument être O(1), plutôt que O(logn).
  • Utilisez multi si vous avez besoin de plusieurs éléments pour avoir la même balise de recherche.

Commande

Si vous avez besoin d'un conteneur d'éléments à toujours trier en fonction d'une opération de comparaison particulière, vous pouvez utiliser un set. Ou un multi_set Si vous avez besoin de plusieurs éléments pour avoir la même valeur.

Ou vous pouvez utiliser un std::vector Trié, mais vous devrez le garder trié.

La stabilité

Lorsque les itérateurs et les références sont invalidés, c'est parfois un problème. Si vous avez besoin d'une liste d'éléments, de sorte que vous ayez des itérateurs/pointeurs vers ces éléments à divers autres endroits, l'approche de l'invalidation de std::vector Peut ne pas être appropriée. Toute opération d'insertion peut entraîner une invalidation, selon la taille et la capacité actuelles.

std::list Offre une garantie ferme: un itérateur et ses références/pointeurs associés ne sont invalidés que lorsque l'article lui-même est retiré du conteneur. std::forward_list Est là si la mémoire est un problème grave.

Si c'est une garantie trop forte, std::deque Offre une garantie plus faible mais utile. L'invalidation résulte des insertions au milieu, mais les insertions en tête ou en queue ne provoquent que l'invalidation des itérateurs , pas des pointeurs/références à des éléments dans le conteneur.

Performance d'insertion

std::vector Ne fournit qu'une insertion bon marché à la fin (et même alors, cela devient cher si vous soufflez de capacité).

std::list Est coûteux en termes de performances (chaque élément nouvellement inséré coûte une allocation de mémoire), mais il est cohérent . Il offre également la possibilité parfois indispensable de mélanger les articles pour pratiquement aucun coût de performance, ainsi que d'échanger des articles avec d'autres conteneurs std::list Du même type sans perte de performances. Si vous devez souvent mélanger , utilisez std::list.

std::deque Permet une insertion/retrait à temps constant au niveau de la tête et de la queue, mais l'insertion au milieu peut être assez coûteuse. Donc, si vous devez ajouter/supprimer des éléments à l'avant comme à l'arrière, std::deque Pourrait être ce dont vous avez besoin.

Il convient de noter que, grâce à la sémantique de déplacement, les performances d'insertion de std::vector Peuvent ne pas être aussi mauvaises qu'auparavant. Certaines implémentations implémentaient une forme de copie d'éléments sémantique basée sur le déplacement (la soi-disant "swaptimisation"), mais maintenant que le déplacement fait partie du langage, il est mandaté par la norme.

Aucune allocation dynamique

std::array Est un bon conteneur si vous voulez le moins d'allocations dynamiques possibles. C'est juste un wrapper autour d'un tableau C; cela signifie que sa taille doit être connue au moment de la compilation . Si vous pouvez vivre avec cela, utilisez std::array.

Cela étant dit, utiliser std::vector Et reserveing une taille fonctionnerait tout aussi bien pour un std::vector Borné. De cette façon, la taille réelle peut varier et vous n'obtenez qu'une seule allocation de mémoire (sauf si vous gaspillez la capacité).

48
Nicol Bolas

Voici la version C++ 11 de l'organigramme ci-dessus. [initialement publié sans attribution à son auteur d'origine, Mikael Persson ]

24
Wasim Thabraze

Voici un tour rapide, bien qu'il ait probablement besoin de travail

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

Vous remarquerez peut-être que cela diffère énormément de la version C++ 03, principalement en raison du fait que je n'aime vraiment pas les nœuds liés. Les conteneurs de nœuds liés peuvent généralement être battus en performances par un conteneur non lié, sauf dans quelques rares situations. Si vous ne savez pas quelles sont ces situations et avez accès à Boost, n'utilisez pas de conteneurs de nœuds liés. (std :: list, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Cette liste se concentre principalement sur les petits et moyens conteneurs, car (A) c'est 99,99% de ce que nous traitons dans le code, et (B) Un grand nombre d'éléments ont besoin d'algorithmes personnalisés, pas de conteneurs différents.

1
Mooing Duck