Come posso selezionare in modo efficiente un contenitore di libreria standard in C ++ 11?


135

C'è un'immagine ben nota (cheat sheet) chiamata "Scelta del contenitore C ++". È un diagramma di flusso per scegliere il contenitore migliore per l'uso desiderato.

Qualcuno sa se esiste già una versione C ++ 11 di esso?

Questo è il precedente: Scelta del contenitore eC ++


6
Non l'ho mai visto prima. Grazie!
WeaselFox,

6
@WeaselFox: fa già parte del C ++ - Faq qui su SO.
Alok Save

4
C ++ 11 ha introdotto solo un nuovo tipo di contenitore vero: i contenitori unordered_X. Includerli confonderebbe considerevolmente la tabella, dato che ci sono una serie di considerazioni al momento di decidere se una tabella hash è appropriata.
Nicol Bolas,

13
James ha ragione, ci sono più casi per usare il vettore di quelli mostrati nella tabella. Il vantaggio della localizzazione dei dati supera in molti casi la mancanza di efficienza in alcune operazioni (più presto C ++ 11). Non trovo la tabella così utile nemmeno per c ++ 03
David Rodríguez - dribeas

33
Questo è carino, ma penso che leggere qualsiasi libro di testo comune sulle strutture di dati ti lascerà in uno stato in cui potresti non solo reinventare questo diagramma di flusso in pochi minuti, ma anche sapere cose molto più utili su cui questo diagramma di flusso lucida.
Andrew Tomazos,

Risposte:


97

Non che io lo sappia, comunque immagino che possa essere fatto testualmente. Inoltre, il grafico è leggermente fuori, perché listnon è un contenitore così buono in generale, e nemmeno lo è forward_list. Entrambi gli elenchi sono contenitori molto specializzati per applicazioni di nicchia.

Per creare un grafico del genere, hai solo bisogno di due semplici linee guida:

  • Scegli prima per la semantica
  • Quando sono disponibili diverse opzioni, scegli la più semplice

Inizialmente preoccuparsi delle prestazioni è di solito inutile. Le grandi considerazioni sulla O entrano davvero in gioco solo quando inizi a gestire alcune migliaia (o più) di oggetti.

Esistono due grandi categorie di contenitori:

  • Contenitori associativi : hanno findun'operazione
  • Contenitori Sequence semplici

e poi si può costruire diversi adattatori su di essi: stack, queue, priority_queue. Lascerò qui gli adattatori, sono sufficientemente specializzati per essere riconoscibili.


Domanda 1: Associativa ?

  • Se è necessario cercare facilmente con una chiave, è necessario un contenitore associativo
  • Se è necessario disporre gli elementi ordinati, è necessario un contenitore associativo ordinato
  • Altrimenti, passa alla domanda 2.

Domanda 1.1: Ordinata ?

  • Se non hai bisogno di un ordine specifico, usa un unordered_container, altrimenti usa la sua controparte ordinata tradizionale.

Domanda 1.2: Chiave separata ?

  • Se la chiave è separata dal valore, utilizzare a map, altrimenti utilizzare aset

Domanda 1.3: duplicati ?

  • Se si desidera conservare i duplicati, utilizzare a multi, altrimenti non.

Esempio:

Supponiamo che io abbia diverse persone a cui è associato un ID univoco e che vorrei recuperare i dati di una persona dal suo ID nel modo più semplice possibile.

  1. Voglio una findfunzione, quindi un contenitore associativo

    1.1. Non me ne potrebbe fregare di meno dell'ordine, quindi un unordered_contenitore

    1.2. La mia chiave (ID) è separata dal valore a cui è associata, quindi amap

    1.3. L'ID è univoco, quindi nessun duplicato dovrebbe insinuarsi.

La risposta finale è: std::unordered_map<ID, PersonData>.


Domanda 2: memoria stabile ?

  • Se gli elementi devono essere stabili in memoria (ad esempio, non dovrebbero spostarsi quando il contenitore stesso viene modificato), quindi utilizzare alcuni list
  • Altrimenti, vai alla domanda 3.

Domanda 2.1: Quale ?

  • Accontentarsi di un list; a forward_listè utile solo per un minore footprint di memoria.

Domanda 3: dimensioni dinamiche ?

  • Se il contenitore ha una dimensione nota (al momento della compilazione) e questa dimensione non verrà modificata nel corso del programma e gli elementi sono predefiniti costruibili o è possibile fornire un elenco di inizializzazione completo (utilizzando la { ... }sintassi), quindi utilizzare un array. Sostituisce il tradizionale C-array, ma con comode funzioni.
  • Altrimenti, vai alla domanda 4.

Domanda 4: doppio attacco ?

  • Se desideri essere in grado di rimuovere elementi sia dalla parte anteriore che da quella posteriore, usa a deque, altrimenti usa a vector.

Noterai che, per impostazione predefinita, a meno che tu non abbia bisogno di un contenitore associativo, la tua scelta sarà a vector. Si scopre che è anche la raccomandazione di Sutter e Stroustrup .


5
+1, ma con alcune note: 1) arraynon richiede un tipo costruibile predefinito; 2) La scelta dei messaggi di posta multielettronica non dipende tanto dal fatto che i duplicati sono consentiti, ma piuttosto dal fatto che mantenerli importanti (puoi mettere i duplicati in non multicontenitori, succede solo che ne viene conservato solo uno).
R. Martinho Fernandes,

2
L'esempio è un po 'fuori. 1) possiamo "trovare" (non la funzione membro, quella "<algoritmo>") su un contenitore non associativo, 1.1) se dobbiamo trovare "in modo efficiente", e unordered_ sarà O (1) e non O ( registro n).
BlakBat,

4
@BlakBat: map.find(key)è molto più appetibile di std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));quanto non sia, quindi è importante, semanticamente, che findsia una funzione membro piuttosto che quella di <algorithm>. Per quanto riguarda O (1) vs O (log n), non influisce sulla semantica; Rimuoverò "in modo efficiente" dall'esempio e lo sostituirò con "facilmente".
Matthieu M.,

"Se gli elementi dovessero essere stabili in memoria ... allora usa un po 'di elenco" ... hmmm, pensavo dequeavessi anche questa proprietà?
Martin Ba,

@MartinBa: Sì e no. In a dequegli elementi sono stabili solo se si preme / pop alle due estremità; se si inizia a inserire / cancellare nel mezzo, fino a N / 2 elementi vengono mescolati per riempire lo spazio creato.
Matthieu M.,

51

Mi piace la risposta di Matthieu, ma ho intenzione di riaffermare il diagramma di flusso come questo:

Quando NON usare std :: vector

Per impostazione predefinita, se hai bisogno di un contenitore di roba, usa std::vector. Pertanto, ogni altro contenitore è giustificato solo fornendo un'alternativa di funzionalità a std::vector.

Costruttori

std::vectorrichiede che il suo contenuto sia costruibile in movimento, poiché deve essere in grado di mescolare gli oggetti in giro. Questo non è un onere gravoso da porre sui contenuti (si noti che i costruttori predefiniti non sono richiesti , grazie emplacee così via). Tuttavia, la maggior parte degli altri contenitori non richiede alcun costruttore particolare (di nuovo, grazie a emplace). Quindi, se si dispone di un oggetto in cui è assolutamente non si può implementare un costruttore mossa, allora si dovrà scegliere qualcos'altro.

A std::dequesarebbe il rimpiazzo generale, avente molte delle proprietà di std::vector, ma è possibile inserire solo su entrambe le estremità del deque. Gli inserti nel mezzo richiedono spostamento. Un std::listposto non richiede alcun contenuto.

Ha bisogno di bool

std::vector<bool>non è. Bene, è standard. Ma non è vectornel solito senso, poiché le operazioni che std::vectornormalmente consentono sono vietate. E sicuramente non contiene bools .

Pertanto, se hai bisogno di un vectorcomportamento reale da un contenitore di bools, non lo otterrai std::vector<bool>. Quindi dovrai fare il dovuto con a std::deque<bool>.

ricerca

Se è necessario trovare elementi in un contenitore e il tag di ricerca non può essere solo un indice, potrebbe essere necessario abbandonare std::vectora favore di sete map. Nota la parola chiave " may "; a std::vectorvolte un ordinato è un'alternativa ragionevole. O Boost.Container's flat_set/map, che implementa un ordinato std::vector.

Esistono ora quattro varianti di queste, ognuna con le proprie esigenze.

  • Usa a mapquando il tag di ricerca non è uguale all'elemento che stai cercando. Altrimenti usa a set.
  • Utilizzare unorderedquando sono presenti molti elementi nel contenitore e le prestazioni di ricerca devono assolutamente essere O(1), anziché O(logn).
  • Utilizzare multise sono necessari più elementi per avere lo stesso tag di ricerca.

ordinazione

Se è necessario ordinare sempre un contenitore di articoli in base a una particolare operazione di confronto, è possibile utilizzare a set. Oppure multi_setse hai bisogno di più articoli per avere lo stesso valore.

Oppure puoi usare un ordinato std::vector, ma dovrai mantenerlo ordinato.

Stabilità

Quando gli iteratori e i riferimenti vengono invalidati, a volte è un problema. Se hai bisogno di un elenco di elementi, in modo tale da avere iteratori / puntatori a quegli elementi in vari altri posti, std::vectorl'approccio all'invalidazione potrebbe non essere appropriato. Qualsiasi operazione di inserimento può causare invalidazioni, a seconda delle dimensioni e della capacità correnti.

std::listoffre una garanzia certa: un iteratore e i relativi riferimenti / puntatori vengono invalidati solo quando l'elemento stesso viene rimosso dal contenitore. std::forward_listc'è se la memoria è una preoccupazione seria.

Se è una garanzia troppo forte, std::dequeoffre una garanzia più debole ma utile. L'invalidazione deriva dagli inserimenti nel mezzo, ma gli inserimenti nella testa o nella coda causano solo l'invalidazione degli iteratori , non dei puntatori / riferimenti agli elementi nel contenitore.

Prestazioni di inserimento

std::vector fornisce solo un inserimento economico alla fine (e anche allora, diventa costoso se si soffia la capacità).

std::listè costoso in termini di prestazioni (ogni elemento appena inserito costa un'allocazione di memoria), ma è coerente . Offre inoltre la possibilità occasionalmente indispensabile di mescolare gli oggetti praticamente senza costi di prestazione, nonché di scambiare oggetti con altri std::listcontenitori dello stesso tipo senza perdita di prestazioni. Se devi mescolare molte cose , usa std::list.

std::dequefornisce inserimento / rimozione a tempo costante nella testa e nella coda, ma l'inserimento nel mezzo può essere piuttosto costoso. Quindi, se hai bisogno di aggiungere / rimuovere oggetti dalla parte anteriore e posteriore, std::dequepotrebbe essere quello che ti serve.

Va notato che, grazie allo spostamento della semantica, le std::vectorprestazioni di inserimento potrebbero non essere peggiori come una volta. Alcune implementazioni hanno implementato una forma di copia degli oggetti basata su semantiche di movimento (la cosiddetta "swaptimization"), ma ora che lo spostamento fa parte del linguaggio, è obbligatorio per lo standard.

Nessuna allocazione dinamica

std::arrayè un ottimo contenitore se si desidera il minor numero possibile di allocazioni dinamiche. È solo un involucro attorno a un array C; questo significa che le sue dimensioni devono essere conosciute in fase di compilazione . Se riesci a convivere con quello, allora usa std::array.

Detto questo, usare std::vectore reserveing di una dimensione funzionerebbe altrettanto bene per un limite std::vector. In questo modo, le dimensioni effettive possono variare e si ottiene solo un'allocazione di memoria (a meno che non si esaurisca la capacità).


1
Beh, anche a me piace molto la tua risposta :) WRT mantenendo un vettore ordinato, a parte std::sort, c'è anche std::inplace_mergeche è interessante posizionare facilmente nuovi elementi (piuttosto che una std::lower_bound+ std::vector::insertchiamata). Bello da imparare flat_sete flat_map!
Matthieu M.,

2
Inoltre, non è possibile utilizzare un vettore con tipi allineati a 16 byte. Anche un buon sostituto vector<bool>è vector<char>.
Inverso,

@Inverso: "Non è inoltre possibile utilizzare un vettore con tipi allineati a 16 byte." Dice chi? Se std::allocator<T>non supporta tale allineamento (e non so perché non lo farebbe), puoi sempre utilizzare il tuo allocatore personalizzato.
Nicol Bolas,

2
@Inverso: C ++ 11 std::vector::resizeha un sovraccarico che non ha valore (richiede solo la nuova dimensione; tutti i nuovi elementi saranno costruiti sul posto di default). Inoltre, perché i compilatori non sono in grado di allineare correttamente i parametri di valore, anche quando viene dichiarato che hanno tale allineamento?
Nicol Bolas,

1
bitsetper bool se conosci le dimensioni in anticipo en.cppreference.com/w/cpp/utility/bitset
bendervader

25

Ecco la versione C ++ 11 del diagramma di flusso sopra. [originariamente pubblicato senza attribuzione al suo autore originale, Mikael Persson ]


2
@NO_NAME Wow, sono contento che qualcuno si sia preso la briga di citare una fonte.
underscore_d

1

Ecco un giro veloce, anche se probabilmente ha bisogno di lavoro

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

Potresti notare che questo differisce enormemente dalla versione C ++ 03, principalmente perché non mi piacciono i nodi collegati. I contenitori di nodi collegati possono in genere essere battuti nelle prestazioni da un contenitore non collegato, tranne in alcune rare situazioni. Se non sai quali siano queste situazioni e hai accesso a boost, non utilizzare contenitori di nodi collegati. (std :: list, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Questo elenco si concentra principalmente su contenitori di piccole e medie dimensioni, perché (A) è il 99,99% di ciò che trattiamo nel codice e (B) Un gran numero di elementi richiede algoritmi personalizzati, non contenitori diversi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.