Bagaimana cara memilih container Library Standar secara efisien di C ++ 11?

136

Ada gambar terkenal (lembar contekan) yang disebut "C ++ Container choice". Ini adalah diagram alir untuk memilih penampung terbaik untuk penggunaan yang diinginkan.

Adakah yang tahu jika sudah ada versi C ++ 11?

Ini yang sebelumnya: eC ++ Pilihan wadah

BlakBat
sumber
6
Belum pernah melihat ini sebelumnya. Terima kasih!
WeaselFox
6
@WeaselFox: Ini sudah menjadi bagian dari C ++ - Faq di SO.
Alok Save
4
C ++ 11 hanya memperkenalkan satu jenis container baru: container unordered_X. Memasukkannya hanya akan mengacaukan tabel, karena ada sejumlah pertimbangan saat memutuskan apakah tabel hash sesuai.
Nicol Bolas
13
James benar, ada lebih banyak kasus menggunakan vektor daripada yang ditunjukkan tabel. Keuntungan dari lokalitas data mengungguli dalam banyak kasus kurangnya efisiensi dalam beberapa operasi (lebih cepat lagi C ++ 11). Saya tidak menemukan grafik yang begitu berguna bahkan untuk c ++ 03
David Rodríguez - dribeas
34
Ini lucu, tetapi saya pikir membaca buku teks umum tentang struktur data akan membuat Anda berada dalam keadaan di mana Anda tidak hanya dapat menemukan kembali diagram alur ini dalam beberapa menit, tetapi juga mengetahui lebih banyak hal berguna yang disorot diagram alur ini.
Andrew Tomazos

Jawaban:

97

Setahu saya tidak, namun itu bisa dilakukan secara tekstual. Juga, bagannya sedikit melenceng, karena listbukan merupakan wadah yang baik secara umum, begitu pula halnya forward_list. Kedua daftar tersebut adalah wadah yang sangat khusus untuk aplikasi khusus.

Untuk membuat bagan seperti itu, Anda hanya perlu dua pedoman sederhana:

  • Pilih semantik terlebih dahulu
  • Ketika beberapa pilihan tersedia, pilih yang paling sederhana

Khawatir tentang kinerja biasanya tidak berguna pada awalnya. Pertimbangan O besar hanya benar-benar muncul ketika Anda mulai menangani beberapa ribu (atau lebih) item.

Ada dua kategori besar wadah:

  • Kontainer asosiatif : mereka memiliki findoperasi
  • Kontainer Urutan sederhana

dan kemudian Anda dapat membangun beberapa adapter di atas mereka: stack, queue, priority_queue. Saya akan meninggalkan adaptornya di sini, mereka cukup terspesialisasi agar dapat dikenali.


Pertanyaan 1: Asosiatif ?

  • Jika Anda perlu mencari dengan mudah dengan satu kunci, maka Anda memerlukan wadah asosiatif
  • Jika Anda perlu mengurutkan elemen, Anda memerlukan wadah asosiatif yang dipesan
  • Jika tidak, lompat ke pertanyaan 2.

Pertanyaan 1.1: Dipesan ?

  • Jika Anda tidak memerlukan pesanan khusus, gunakan unordered_wadah, jika tidak gunakan pesanan pesanan tradisionalnya.

Pertanyaan 1.2: Kunci Terpisah ?

  • Jika kunci terpisah dari nilainya, gunakan a map, jika tidak gunakan aset

Pertanyaan 1.3: Duplikat ?

  • Jika Anda ingin menyimpan duplikat, gunakan a multi, jika tidak, jangan.

Contoh:

Misalkan saya memiliki beberapa orang dengan ID unik yang terkait dengannya, dan saya ingin mengambil data seseorang dari ID-nya sesederhana mungkin.

  1. Saya ingin sebuah findfungsi, jadi wadah asosiatif

    1.1. Saya tidak peduli tentang pesanan, jadi sebuah unordered_wadah

    1.2. Kunci saya (ID) terpisah dari nilai yang diasosiasikan dengannya, jadi amap

    1.3. ID-nya unik, jadi tidak ada duplikat yang masuk.

Jawaban akhir adalah: std::unordered_map<ID, PersonData>.


Pertanyaan 2: Memori stabil ?

  • Jika elemen harus stabil dalam memori (yaitu, mereka tidak boleh berpindah-pindah saat wadah itu sendiri dimodifikasi), maka gunakan beberapa list
  • Jika tidak, lompat ke pertanyaan 3.

Pertanyaan 2.1: Yang mana ?

  • Setel untuk a list; a forward_listhanya berguna untuk footprint memori yang lebih rendah.

Pertanyaan 3: Berukuran dinamis ?

  • Jika penampung memiliki ukuran yang diketahui (pada waktu kompilasi), dan ukuran ini tidak akan diubah selama program berjalan, dan elemennya dapat dibangun secara default atau Anda dapat memberikan daftar inisialisasi lengkap (menggunakan { ... }sintaks), lalu gunakan array. Ini menggantikan C-array tradisional, tetapi dengan fungsi yang nyaman.
  • Jika tidak, lompat ke pertanyaan 4.

Pertanyaan 4: Berujung ganda ?

  • Jika Anda ingin menghapus item dari depan dan belakang, gunakan a deque, jika tidak gunakan a vector.

Anda akan mencatat bahwa, secara default, kecuali Anda membutuhkan wadah asosiatif, pilihan Anda adalah a vector. Ternyata itu juga rekomendasi Sutter dan Stroustrup .

Matthieu M.
sumber
5
+1, tetapi dengan beberapa catatan: 1) arraytidak memerlukan tipe konstruktif default; 2) memilih multis tidak terlalu banyak tentang duplikat yang diizinkan tetapi lebih tentang apakah menyimpannya penting (Anda dapat meletakkan duplikat di non- multicontainer, kebetulan hanya satu yang disimpan).
R. Martinho Fernandes
2
Contohnya agak salah. 1) kita dapat "menemukan" (bukan fungsi anggota, "<algorithm>" satu) pada wadah non asosiatif, 1.1) jika kita perlu menemukan "efisien", dan unordered_ akan menjadi O (1) dan bukan O ( log n).
BlakBat
4
@BlakBat: map.find(key)jauh lebih enak dari pada std::find(map.begin(), map.end(), [&](decltype(map.front()) p) { return p.first < key; }));meskipun, oleh karena itu penting, secara semantik, itu findadalah fungsi anggota dan bukan dari <algorithm>. Adapun O (1) vs O (log n), itu tidak mempengaruhi semantik; Saya akan menghapus "efisien" dari contoh dan menggantinya dengan "dengan mudah".
Matthieu M.
"Jika elemen harus stabil di memori ... maka gunakan beberapa daftar" ... hmmm, saya pikir dequepunya properti ini juga?
Martin Ba
@MartinBa: Ya dan tidak. Dalam sebuah dequeelemen stabil hanya jika Anda menekan / pop di kedua ujungnya; jika Anda mulai memasukkan / menghapus di tengah maka hingga N / 2 elemen dikocok untuk mengisi celah yang dibuat.
Matthieu M.
51

Saya suka jawaban Matthieu, tetapi saya akan mengulangi diagram alurnya seperti ini:

Kapan TIDAK menggunakan std :: vector

Secara default, jika Anda membutuhkan wadah barang, gunakan std::vector. Dengan demikian, setiap penampung lain hanya dibenarkan dengan menyediakan beberapa alternatif fungsionalitas std::vector.

Konstruktor

std::vectormengharuskan isinya dapat dipindahkan, karena harus dapat mengocok item di sekitar. Ini bukan beban yang berat untuk ditempatkan pada konten (perhatikan bahwa konstruktor default tidak diperlukan , berkat emplacedan sebagainya). Namun, sebagian besar wadah lain tidak memerlukan konstruktor tertentu (sekali lagi, terima kasih emplace). Jadi jika Anda memiliki objek di mana Anda sama sekali tidak dapat mengimplementasikan konstruktor pemindahan, maka Anda harus memilih yang lain.

A std::dequeakan menjadi pengganti umum, memiliki banyak properti std::vector, tetapi Anda hanya dapat memasukkan di kedua ujung deque. Sisipan di tengah perlu dipindahkan. A std::listtidak mensyaratkan isinya.

Membutuhkan Bools

std::vector<bool>tidak. Ya, ini standar. Tapi itu bukan vectordalam arti biasa, karena operasi yang std::vectorbiasanya memungkinkan dilarang. Dan itu pasti tidak mengandung bools .

Oleh karena itu, jika Anda memerlukan vectorperilaku nyata dari wadah bools, Anda tidak akan mendapatkannya std::vector<bool>. Jadi, Anda harus membayar dengan a std::deque<bool>.

Mencari

Jika Anda perlu menemukan elemen dalam sebuah wadah, dan tag pencarian tidak bisa hanya berupa indeks, maka Anda mungkin perlu meninggalkannya std::vectordemi setdan map. Perhatikan kata kunci " mungkin "; penyortiran std::vectorterkadang merupakan alternatif yang masuk akal. Atau Boost.Container's flat_set/map, yang mengimplementasikan file std::vector.

Sekarang ada empat variasi, masing-masing dengan kebutuhannya sendiri.

  • Gunakan mapjika tag pencarian tidak sama dengan item yang Anda cari sendiri. Jika tidak, gunakan a set.
  • Gunakan unorderedjika Anda memiliki banyak item dalam penampung dan kinerja penelusuran benar-benar perlu O(1), bukan O(logn).
  • Gunakan multijika Anda membutuhkan banyak item untuk memiliki tag pencarian yang sama.

Memerintah

Jika Anda membutuhkan penampung item untuk selalu diurutkan berdasarkan operasi perbandingan tertentu, Anda dapat menggunakan file set. Atau multi_setjika Anda membutuhkan beberapa item untuk memiliki nilai yang sama.

Atau Anda dapat menggunakan penyortiran std::vector, tetapi Anda harus tetap menyortirnya.

Stabilitas

Ketika iterator dan referensi tidak valid terkadang menjadi perhatian. Jika Anda memerlukan daftar item, sehingga Anda memiliki iterator / penunjuk ke item tersebut di berbagai tempat lain, maka std::vectorpendekatan pembatalan mungkin tidak tepat. Setiap operasi penyisipan dapat menyebabkan pembatalan, tergantung pada ukuran dan kapasitas saat ini.

std::listmenawarkan jaminan pasti: iterator dan referensi / petunjuk terkaitnya hanya tidak valid saat item itu sendiri dikeluarkan dari wadah. std::forward_listapakah ada jika ingatan menjadi perhatian serius.

Jika itu jaminan yang terlalu kuat, std::dequetawarkan jaminan yang lebih lemah tapi berguna. Hasil invalidasi dari penyisipan di tengah, tetapi penyisipan di kepala atau ekor hanya menyebabkan pembatalan iterator , bukan penunjuk / referensi ke item dalam penampung.

Kinerja Penyisipan

std::vector hanya menyediakan penyisipan murah di akhir (dan bahkan kemudian, menjadi mahal jika Anda meniup kapasitas).

std::listmahal dalam hal performa (setiap item yang baru dimasukkan memerlukan alokasi memori), tetapi konsisten . Ini juga menawarkan kemampuan yang kadang-kadang sangat diperlukan untuk mengocok barang-barang dengan hampir tanpa biaya kinerja, serta untuk menukar barang dengan std::listwadah lain dari jenis yang sama tanpa kehilangan kinerja. Jika Anda perlu untuk mengocok hal-hal di sekitar banyak , penggunaan std::list.

std::dequemenyediakan penyisipan / pemindahan waktu konstan di kepala dan ekor, tetapi penyisipan di tengah bisa cukup mahal. Jadi jika Anda perlu menambah / menghapus barang dari depan maupun belakang, std::dequemungkin yang Anda butuhkan.

Perlu dicatat bahwa, berkat semantik pemindahan, std::vectorkinerja penyisipan mungkin tidak seburuk dulu. Beberapa implementasi menerapkan bentuk penyalinan item berbasis semantik pemindahan (yang disebut "swaptimization"), tetapi sekarang pemindahan adalah bagian dari bahasa, itu diamanatkan oleh standar.

Tidak Ada Alokasi Dinamis

std::arrayadalah wadah yang bagus jika Anda menginginkan alokasi dinamis sesedikit mungkin. Ini hanya pembungkus di sekitar C-array; ini berarti ukurannya harus diketahui pada saat kompilasi . Jika Anda bisa hidup dengan itu, maka gunakan std::array.

Karena itu, menggunakan std::vectordan reservemengukur ukuran akan bekerja dengan baik untuk yang dibatasi std::vector. Dengan cara ini, ukuran sebenarnya dapat bervariasi, dan Anda hanya mendapatkan satu alokasi memori (kecuali jika kapasitasnya habis).

Nicol Bolas
sumber
1
Yah, saya juga suka jawaban Anda :) WRT menjaga vektor diurutkan, selain dari std::sort, ada juga std::inplace_mergeyang menarik untuk dengan mudah menempatkan elemen baru (daripada panggilan std::lower_bound+ std::vector::insert). Senang belajar tentang flat_setdan flat_map!
Matthieu M.
2
Anda juga tidak dapat menggunakan vektor dengan tipe selaras 16-byte. Juga pengganti yang bagus untuk vector<bool>itu vector<char>.
Terbalik
@Inverse: "Anda juga tidak dapat menggunakan vektor dengan tipe selaras 16-byte." Kata siapa? Jika std::allocator<T>tidak mendukung perataan itu (dan saya tidak tahu mengapa tidak), maka Anda selalu dapat menggunakan pengalokasi khusus Anda sendiri.
Nicol Bolas
2
@Inverse: C ++ 11 std::vector::resizememiliki kelebihan beban yang tidak mengambil nilai (hanya mengambil ukuran baru; elemen baru akan dibuat secara default di tempat). Juga, mengapa kompiler tidak dapat menyelaraskan parameter nilai dengan benar, bahkan ketika mereka dinyatakan memiliki keselarasan itu?
Nicol Bolas
1
bitsetuntuk bool jika Anda mengetahui ukurannya terlebih dahulu en.cppreference.com/w/cpp/utility/bitset
bendervader
25

Berikut adalah versi C ++ 11 dari diagram alur di atas. [awalnya diposting tanpa atribusi ke penulis aslinya, Mikael Persson ]

Wasim Thabraze
sumber
2
@NO_NAME Wow, aku senang seseorang mau repot-repot mengutip sebuah sumber.
underscore_d
1

Ini putaran cepat, meskipun mungkin perlu diperbaiki

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

Anda mungkin memperhatikan bahwa ini sangat berbeda dari versi C ++ 03, terutama karena fakta bahwa saya benar-benar tidak menyukai node yang terhubung. Penampung node yang ditautkan biasanya dapat dikalahkan kinerjanya oleh penampung yang tidak tertaut, kecuali dalam beberapa situasi yang jarang terjadi. Jika Anda tidak tahu apa situasi tersebut, dan memiliki akses untuk meningkatkan, jangan gunakan wadah node tertaut. (std :: list, std :: slist, std :: map, std :: multimap, std :: set, std :: multiset). Daftar ini sebagian besar berfokus pada penampung sisi kecil dan menengah, karena (A) itu 99,99% dari apa yang kita tangani dalam kode, dan (B) Sejumlah besar elemen membutuhkan algoritme khusus, bukan penampung yang berbeda.

Mooing Duck
sumber