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:
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.
Apakah masalah ini diketahui secara luas, karena tentunya mengupgrade compiler akan menyebabkan kode yang dulu berfungsi tiba-tiba berhenti bekerja?
Apakah komite standar C ++ menginginkan perilaku ini?
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.
a
tidak. Itu harus membuat salinan. Juga, perilaku ini sepenuhnya bergantung pada stdlib, bukan kompilernya.4.9.0 20131223 (experimental)
masing - masing. Outputnyaa.second is now 0x2074088
(atau serupa) untuk saya.Jawaban:
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-603bits/unordered_map.h
, baris 365-370Yang terakhir ini salah menggunakan
std::move
tempat yang seharusnya digunakanstd::forward
.sumber
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.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.Karena Anda, seperti banyak orang sebelumnya, salah mengartikan
value_type
wadah itu. Thevalue_type
of*map
(baik dipesan atau tidak) adalahpair<const K, T>
, yang dalam kasus Anda adalahpair<const int, shared_ptr<int>>
. Jenis yang tidak cocok menghilangkan kelebihan beban yang mungkin Anda harapkan:sumber
std::move
itu tidak bergerak sama sekali.std::forward
untuk 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).&&
; 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.