Apakah dimaksudkan oleh komite standar C ++ bahwa dalam C ++ 11 unordered_map menghancurkan apa yang disisipkan?

114

Saya baru saja kehilangan tiga hari dalam hidup saya melacak bug yang sangat aneh di mana unordered_map :: insert () menghancurkan variabel yang Anda masukkan. Perilaku yang sangat tidak jelas ini terjadi hanya pada kompiler terbaru: Saya menemukan bahwa clang 3.2-3.4 dan GCC 4.8 adalah satu - satunya kompiler yang mendemonstrasikan "fitur" ini.

Berikut beberapa kode yang dikurangi dari basis kode utama saya yang menunjukkan masalah tersebut:

#include <memory>
#include <unordered_map>
#include <iostream>

int main(void)
{
  std::unordered_map<int, std::shared_ptr<int>> map;
  auto a(std::make_pair(5, std::make_shared<int>(5)));
  std::cout << "a.second is " << a.second.get() << std::endl;
  map.insert(a); // Note we are NOT doing insert(std::move(a))
  std::cout << "a.second is now " << a.second.get() << std::endl;
  return 0;
}

Saya, seperti kebanyakan programmer C ++, mengharapkan output terlihat seperti ini:

a.second is 0x8c14048
a.second is now 0x8c14048

Tetapi dengan dentang 3.2-3.4 dan GCC 4.8 saya mendapatkan ini sebagai gantinya:

a.second is 0xe03088
a.second is now 0

Yang mungkin tidak masuk akal, sampai Anda memeriksa secara dekat dokumen untuk unordered_map :: insert () di http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ di mana overload no 2 adalah:

template <class P> pair<iterator,bool> insert ( P&& val );

Yang merupakan pemindahan referensi universal serakah, memakan apa pun yang tidak cocok dengan kelebihan beban lainnya, dan memindahkan pembuatannya ke dalam sebuah nilai_tipe. Jadi mengapa kode kita di atas memilih overload ini, dan bukan overload unordered_map :: value_type seperti yang mungkin diharapkan sebagian besar?

Jawabannya menatap Anda langsung: unordered_map :: value_type adalah pasangan < const int, std :: shared_ptr> dan kompilator akan berpikir dengan benar bahwa pasangan < int , std :: shared_ptr> tidak dapat dikonversi. Oleh karena itu kompilator memilih overload referensi universal move, dan itu menghancurkan yang asli, meskipun pemrogram tidak menggunakan std :: move () yang merupakan konvensi umum untuk menunjukkan bahwa Anda setuju dengan variabel yang dimusnahkan. Oleh karena itu perilaku merusak penyisipan sebenarnya benar sesuai dengan standar C ++ 11, dan kompiler lama salah .

Anda mungkin dapat melihat sekarang mengapa saya memerlukan tiga hari untuk mendiagnosis bug ini. Itu sama sekali tidak jelas dalam basis kode besar di mana tipe yang dimasukkan ke dalam unordered_map adalah typedef yang didefinisikan jauh dalam istilah kode sumber, dan tidak pernah terpikir oleh siapa pun untuk memeriksa apakah typedef identik dengan value_type.

Jadi pertanyaan saya untuk Stack Overflow:

  1. Mengapa kompiler lama tidak menghancurkan variabel yang dimasukkan seperti kompiler baru? Maksud saya, bahkan GCC 4.7 tidak melakukan ini, dan itu cukup memenuhi standar.

  2. Apakah masalah ini diketahui secara luas, karena tentunya mengupgrade compiler akan menyebabkan kode yang dulu berfungsi tiba-tiba berhenti bekerja?

  3. Apakah komite standar C ++ menginginkan perilaku ini?

  4. Bagaimana Anda menyarankan agar unordered_map :: insert () dimodifikasi untuk memberikan perilaku yang lebih baik? Saya menanyakan hal ini karena jika ada dukungan di sini, saya bermaksud untuk menyampaikan perilaku ini sebagai catatan N ke WG21 dan meminta mereka untuk menerapkan perilaku yang lebih baik.

Niall Douglas
sumber
10
Hanya karena itu menggunakan ref universal tidak berarti nilai yang dimasukkan selalu dipindahkan - itu seharusnya hanya dilakukan untuk rvalues, yang biasa atidak. Itu harus membuat salinan. Juga, perilaku ini sepenuhnya bergantung pada stdlib, bukan kompilernya.
Xeo
10
Itu tampak seperti bug dalam implementasi perpustakaan
David Rodríguez - dribeas
4
"Oleh karena itu perilaku perusakan sisipan sebenarnya benar sesuai dengan standar C ++ 11, dan kompiler yang lebih lama salah." Maaf, tapi Anda salah. Dari bagian C ++ Standard mana Anda mendapatkan ide itu? BTW cplusplus.com tidak resmi.
Ben Voigt
1
Saya tidak dapat mereproduksi ini di sistem saya, dan saya menggunakan gcc 4.8.2 dan 4.9.0 20131223 (experimental)masing - masing. Outputnya a.second is now 0x2074088 (atau serupa) untuk saya.
47
Ini adalah bug GCC 57619 , regresi dalam seri 4.8 yang diperbaiki untuk 4.8.2 pada tahun 2013-06.
Casey

Jawaban:

83

Seperti yang ditunjukkan orang lain dalam komentar, konstruktor "universal" sebenarnya tidak diharapkan untuk selalu bergerak dari argumennya. Ini seharusnya berpindah jika argumennya benar-benar sebuah nilai r, dan salin jika itu nilai l.

Perilaku, yang Anda amati, yang selalu bergerak, adalah bug di libstdc ++, yang sekarang telah diperbaiki sesuai dengan komentar pada pertanyaan. Bagi mereka yang penasaran, saya melihat header g ++ - 4.8.

bits/stl_map.h, baris 598-603

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_t._M_insert_unique(std::forward<_Pair>(__x)); }

bits/unordered_map.h, baris 365-370

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

Yang terakhir ini salah menggunakan std::movetempat yang seharusnya digunakan std::forward.

Brian
sumber
11
Clang menggunakan libstdc ++ secara default, stdlib GCC.
Xeo
Saya menggunakan gcc 4.9 dan sedang mencari libstdc++-v3/include/bits/. Saya tidak melihat hal yang sama. Aku mengerti { return _M_h.insert(std::forward<_Pair>(__x)); }. Ini bisa berbeda untuk 4.8, tetapi saya belum memeriksanya.
Ya, jadi saya rasa mereka memperbaiki bugnya.
Brian
@ Brian Nope, saya baru saja memeriksa header sistem saya. Hal yang sama. 4.8.2 btw.
Milik saya 4.8.1, jadi saya rasa itu sudah diperbaiki di antara keduanya.
Brian
20
template <class P> pair<iterator,bool> insert ( P&& val );

Yang merupakan pemindahan referensi universal serakah, memakan apa pun yang tidak cocok dengan kelebihan beban lainnya, dan memindahkannya ke dalam sebuah nilai_jenis.

Itulah yang oleh sebagian orang disebut sebagai referensi universal , tetapi sebenarnya referensi sudah runtuh . Dalam kasus Anda, di mana argumennya adalah lvalue type, pair<int,shared_ptr<int>>itu tidak akan menghasilkan argumen menjadi referensi rvalue dan tidak boleh berpindah darinya.

Jadi mengapa kode kita di atas memilih overload ini, dan bukan overload unordered_map :: value_type seperti yang mungkin diharapkan sebagian besar?

Karena Anda, seperti banyak orang sebelumnya, salah mengartikan value_typewadah itu. The value_typeof *map(baik dipesan atau tidak) adalah pair<const K, T>, yang dalam kasus Anda adalah pair<const int, shared_ptr<int>>. Jenis yang tidak cocok menghilangkan kelebihan beban yang mungkin Anda harapkan:

iterator       insert(const_iterator hint, const value_type& obj);
David Rodríguez - dribeas
sumber
Saya masih tidak mengerti mengapa lemma baru ini ada, "referensi universal", tidak berarti sesuatu yang spesifik, juga tidak membawa nama yang baik untuk sesuatu yang sama sekali tidak "universal" dalam praktiknya. Jauh lebih baik untuk mengingat beberapa aturan lama yang runtuh yang merupakan bagian dari perilaku bahasa-meta C ++ seperti semenjak templat diperkenalkan dalam bahasa tersebut, ditambah tanda tangan baru dari standar C ++ 11. Apa untungnya membicarakan referensi universal ini? Sudah ada nama dan hal yang cukup aneh, seperti std::moveitu tidak bergerak sama sekali.
pengguna2485710
2
@ user2485710 Kadang-kadang, ketidaktahuan adalah kebahagiaan, dan memiliki istilah payung "referensi universal" pada semua referensi yang runtuh dan tweak pengurangan jenis template yang IMHO cukup tidak intuitif, ditambah persyaratan yang digunakan std::forwarduntuk membuat tweak itu melakukan pekerjaan yang sebenarnya ... Scott Meyers telah melakukan pekerjaan yang baik dengan menetapkan aturan yang cukup mudah untuk penerusan (penggunaan referensi universal).
Mark Garcia
1
Dalam pemikiran saya, "referensi universal" adalah pola untuk mendeklarasikan parameter template fungsi yang dapat mengikat baik lvalues ​​maupun rvalues. "Referensi menciutkan" adalah apa yang terjadi saat mengganti (disimpulkan atau ditentukan) parameter template ke dalam definisi, dalam situasi "referensi universal" dan konteks lainnya.
aschepler
2
@aschepler: referensi universal hanyalah nama mewah untuk subset referensi yang runtuh. Saya setuju dengan ketidaktahuan adalah kebahagiaan , dan juga fakta bahwa memiliki nama yang mewah membuat pembicaraan tentang hal itu lebih mudah dan trendi dan itu mungkin membantu menyebarkan perilaku. Itu dikatakan saya bukan penggemar berat nama itu, karena mendorong aturan yang sebenarnya ke sudut di mana mereka tidak perlu diketahui ... yaitu, sampai Anda menemukan kasus di luar apa yang dijelaskan Scott Meyers.
David Rodríguez - dribeas
Semantik tidak berguna sekarang, tetapi perbedaan yang saya coba sarankan: Referensi universal terjadi ketika saya merancang dan mendeklarasikan parameter fungsi sebagai deducible-template-parameter &&; penciutan referensi terjadi saat kompilator membuat instance template. Referensi runtuh adalah alasan referensi universal berfungsi, tetapi otak saya tidak suka menempatkan dua istilah dalam domain yang sama.
aschepler