มีเจตนาโดยคณะกรรมการมาตรฐาน C ++ หรือไม่ว่าใน C ++ 11 unordered_map ทำลายสิ่งที่แทรก?


114

ฉันเพิ่งเสียชีวิตไปสามวันในการติดตามข้อผิดพลาดที่แปลกมากที่ unordered_map :: insert () ทำลายตัวแปรที่คุณแทรก ลักษณะการทำงานที่ไม่ชัดเจนนี้เกิดขึ้นในคอมไพเลอร์ล่าสุดเท่านั้น: ฉันพบว่า clang 3.2-3.4 และ GCC 4.8 เป็นคอมไพเลอร์เพียงตัวเดียวที่แสดง "คุณลักษณะ" นี้

นี่คือโค้ดที่ลดลงบางส่วนจากฐานรหัสหลักของฉันซึ่งแสดงให้เห็นถึงปัญหา:

#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;
}

ฉันเช่นเดียวกับโปรแกรมเมอร์ C ++ ส่วนใหญ่คาดว่าผลลัพธ์จะมีลักษณะดังนี้:

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

แต่ด้วยเสียงดังกราว 3.2-3.4 และ GCC 4.8 ฉันได้รับสิ่งนี้แทน:

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

ซึ่งอาจไม่สมเหตุสมผลจนกว่าคุณจะตรวจสอบเอกสารสำหรับ unordered_map :: insert () อย่างละเอียดที่http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/โดยที่ overload no 2 คือ:

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

ซึ่งเป็นการย้ายข้อมูลอ้างอิงสากลแบบโลภมากเกินไปใช้สิ่งใดก็ตามที่ไม่ตรงกับโอเวอร์โหลดอื่น ๆ และย้ายการสร้างไปยัง value_type เหตุใดโค้ดของเราด้านบนจึงเลือกโอเวอร์โหลดนี้และไม่ใช่ unordered_map :: value_type overload อย่างที่คาดหวังมากที่สุด

คำตอบจ้องคุณตรงหน้า: unordered_map :: value_type เป็นคู่ < const int, std :: shared_ptr> และคอมไพเลอร์จะคิดว่าคู่ < int , std :: shared_ptr> ไม่สามารถแปลงได้ ดังนั้นคอมไพลเลอร์จึงเลือกการโอเวอร์โหลดการอ้างอิงสากลของการย้ายและทำลายต้นฉบับแม้ว่าโปรแกรมเมอร์จะไม่ใช้ std :: move () ซึ่งเป็นแบบแผนทั่วไปในการระบุว่าคุณโอเคกับการที่ตัวแปรถูกทำลาย ดังนั้นแทรกพฤติกรรมทำลายในความเป็นจริงที่ถูกต้องตามที่ C ++ 11 มาตรฐานและคอมไพเลอร์พี่ที่ไม่ถูกต้อง

ตอนนี้คุณคงเห็นแล้วว่าทำไมฉันถึงใช้เวลาสามวันในการวินิจฉัยจุดบกพร่องนี้ มันไม่ชัดเจนเลยในฐานรหัสขนาดใหญ่ที่ประเภทที่ถูกแทรกลงใน unordered_map คือ typedef ที่กำหนดไว้ห่างไกลในแง่ของซอร์สโค้ดและไม่เคยเกิดขึ้นกับใครเพื่อตรวจสอบว่า typedef เหมือนกับ value_type หรือไม่

ดังนั้นคำถามของฉันเกี่ยวกับ Stack Overflow:

  1. เหตุใดคอมไพเลอร์รุ่นเก่าจึงไม่ทำลายตัวแปรที่แทรกเช่นคอมไพเลอร์รุ่นใหม่ ฉันหมายความว่าแม้แต่ GCC 4.7 ก็ไม่ทำเช่นนี้และเป็นไปตามมาตรฐานที่ค่อนข้างดี

  2. ปัญหานี้เป็นที่รู้จักกันอย่างแพร่หลายหรือไม่เพราะการอัพเกรดคอมไพเลอร์จะทำให้โค้ดที่ใช้ทำงานหยุดทำงานกะทันหันหรือไม่?

  3. คณะกรรมการมาตรฐาน C ++ ตั้งใจให้เกิดพฤติกรรมนี้หรือไม่?

  4. คุณจะแนะนำให้แก้ไข unordered_map :: insert () อย่างไรเพื่อให้พฤติกรรมดีขึ้น ฉันถามสิ่งนี้เพราะหากมีการสนับสนุนที่นี่ฉันตั้งใจที่จะส่งพฤติกรรมนี้เป็นหมายเหตุ N ถึง WG21 และขอให้พวกเขาปรับใช้พฤติกรรมที่ดีขึ้น


10
เพียงเพราะใช้การอ้างอิงสากลไม่ได้หมายความว่าค่าที่แทรกจะถูกย้ายเสมอ - ควรทำเช่นนั้นสำหรับ rvalues ​​เท่านั้นซึ่งธรรมดาaไม่ใช่ ควรทำสำเนา นอกจากนี้พฤติกรรมนี้ขึ้นอยู่กับ stdlib ไม่ใช่คอมไพเลอร์
Xeo

10
ดูเหมือนว่าจะเป็นข้อบกพร่องในการนำห้องสมุดไปใช้
David Rodríguez - dribeas

4
"ดังนั้นพฤติกรรมการทำลายเม็ดมีดจึงถูกต้องตามมาตรฐาน C ++ 11 และคอมไพเลอร์รุ่นเก่าไม่ถูกต้อง" ขออภัยคุณคิดผิด คุณได้แนวคิดนั้นมาจากส่วนใดของมาตรฐาน C ++ BTW cplusplus.com ไม่เป็นทางการ
Ben Voigt

1
ฉันไม่สามารถทำซ้ำสิ่งนี้ในระบบของฉันและฉันใช้ gcc 4.8.2 และ4.9.0 20131223 (experimental)ตามลำดับ ผลลัพธ์คือa.second is now 0x2074088 (หรือคล้ายกัน) สำหรับฉัน

47
นี่คือข้อบกพร่องของ GCC 57619ซึ่งเป็นการถดถอยในชุด 4.8 ที่ได้รับการแก้ไขสำหรับ 4.8.2 ในปี 2556-06
Casey

คำตอบ:


83

ตามที่คนอื่น ๆ ได้ชี้ให้เห็นในความคิดเห็นตัวสร้าง "สากล" ไม่ได้ควรจะย้ายจากการโต้แย้งเสมอไป มันควรจะย้ายถ้าอาร์กิวเมนต์เป็นค่า rvalue จริงๆและคัดลอกถ้าเป็น lvalue

พฤติกรรมที่คุณสังเกตเห็นซึ่งเคลื่อนไหวตลอดเวลาเป็นข้อบกพร่องใน libstdc ++ ซึ่งตอนนี้ได้รับการแก้ไขตามความคิดเห็นในคำถาม สำหรับผู้ที่อยากรู้อยากเห็นฉันลองดูที่ส่วนหัว g ++ - 4.8

bits/stl_map.h, สาย 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, สาย 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)); }

หลังใช้ไม่ถูกต้องstd::moveว่าควรใช้ตรงstd::forwardไหน


11
เสียงดังใช้ libstdc ++ โดยค่าเริ่มต้นคือ stdlib ของ GCC
Xeo

ฉันใช้ GCC 4.9 libstdc++-v3/include/bits/และฉันกำลังมองใน ไม่เห็นเหมือนกันเลย { return _M_h.insert(std::forward<_Pair>(__x)); }ฉันเห็น มันอาจแตกต่างกันสำหรับ 4.8 แต่ฉันยังไม่ได้ตรวจสอบ

ใช่ฉันเดาว่าพวกเขาแก้ไขข้อบกพร่องแล้ว
Brian

@Brian Nope ฉันเพิ่งตรวจสอบส่วนหัวของระบบ เหมือนกัน. 4.8.2 btw.

4.8.1 ของฉันฉันเดาว่ามันคงที่ระหว่างทั้งสอง
Brian

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

ซึ่งเป็นการย้ายข้อมูลอ้างอิงสากลแบบโลภมากเกินไปใช้สิ่งใดก็ตามที่ไม่ตรงกับโอเวอร์โหลดอื่น ๆ และย้ายการสร้างไปยัง value_type

นั่นคือสิ่งที่บางคนเรียกการอ้างอิงสากลแต่จริงๆคือยุบอ้างอิง ในกรณีของคุณโดยที่อาร์กิวเมนต์เป็นประเภทlvaluepair<int,shared_ptr<int>>จะไม่ส่งผลให้อาร์กิวเมนต์เป็นการอ้างอิง rvalue และไม่ควรย้ายจากอาร์กิวเมนต์

เหตุใดโค้ดของเราด้านบนจึงเลือกโอเวอร์โหลดนี้และไม่ใช่ unordered_map :: value_type overload อย่างที่คาดหวังมากที่สุด

เนื่องจากคุณเป็นคนอื่น ๆ ก่อนหน้านี้ตีความvalue_typeในคอนเทนเนอร์ผิด value_typeของ*map(ไม่ว่าจะสั่งซื้อหรือสั่ง) เป็นซึ่งในกรณีของคุณคือpair<const K, T> pair<const int, shared_ptr<int>>ประเภทที่ไม่ตรงกันจะช่วยลดการโอเวอร์โหลดที่คุณอาจคาดหวัง:

iterator       insert(const_iterator hint, const value_type& obj);

ฉันยังไม่เข้าใจว่าทำไมคำศัพท์ใหม่นี้จึงมีอยู่ "การอ้างอิงสากล" ไม่ได้หมายถึงอะไรที่เฉพาะเจาะจงและไม่มีชื่อที่ดีสำหรับสิ่งที่ไม่ "สากล" โดยสิ้นเชิงในทางปฏิบัติ จะดีกว่ามากที่จะจำกฎการยุบแบบเก่าซึ่งเป็นส่วนหนึ่งของลักษณะการทำงานของภาษาเมตา C ++ เหมือนเดิมตั้งแต่มีการใช้เทมเพลตในภาษารวมถึงลายเซ็นใหม่จากมาตรฐาน C ++ 11 ประโยชน์ของการพูดถึงการอ้างอิงสากลนี้คืออะไร? มีชื่อและอะไรแปลก ๆ อยู่แล้วแบบstd::moveว่าไม่ขยับอะไรเลย
user2485710

2
@ user2485710 บางครั้งความไม่รู้ก็เป็นความสุขและการมีคำว่า "อ้างอิงสากล" เป็นร่มในการยุบการอ้างอิงทั้งหมดและการปรับเปลี่ยนประเภทเทมเพลตที่ปรับแต่ง IMHO โดยไม่ได้ตั้งใจบวกกับข้อกำหนดที่จะใช้std::forwardเพื่อให้การปรับแต่งนั้นทำงานได้จริง ... Scott Meyers ได้ตั้งกฎที่ค่อนข้างตรงไปตรงมาสำหรับการส่งต่อ (การใช้การอ้างอิงสากล)
Mark Garcia

1
ในความคิดของฉัน "การอ้างอิงสากล" เป็นรูปแบบสำหรับการประกาศพารามิเตอร์เทมเพลตฟังก์ชันที่สามารถเชื่อมโยงกับทั้ง lvalues ​​และ rvalues "การยุบการอ้างอิง" คือสิ่งที่เกิดขึ้นเมื่อแทนที่พารามิเตอร์เทมเพลต (อนุมานหรือระบุ) ลงในคำจำกัดความในสถานการณ์ "การอ้างอิงสากล" และบริบทอื่น ๆ
aschepler

2
@aschepler: การอ้างอิงสากลเป็นเพียงชื่อแฟนซีสำหรับการยุบส่วนย่อยของการอ้างอิง ฉันเห็นด้วยกับความไม่รู้คือความสุขและการมีชื่อที่หรูหราทำให้การพูดถึงเรื่องนี้ง่ายขึ้นและทันสมัยขึ้นและอาจช่วยเผยแพร่พฤติกรรมได้ ที่ถูกกล่าวว่าฉันไม่ใช่แฟนตัวยงของชื่อนี้เนื่องจากมันผลักดันกฎที่แท้จริงไปยังมุมที่พวกเขาไม่จำเป็นต้องรู้ ... นั่นคือจนกว่าคุณจะโดนคดีนอกสิ่งที่ Scott Meyers อธิบาย
David Rodríguez - น้ำลายไหล

ความหมายไม่มีจุดหมายในขณะนี้ แต่ความแตกต่างที่ผมพยายามที่จะแนะนำ: การอ้างอิงสากลที่เกิดขึ้นเมื่อฉันกำลังออกแบบและประกาศพารามิเตอร์ของฟังก์ชันเป็น deducible แม่แบบพารามิเตอร์&&; การยุบการอ้างอิงเกิดขึ้นเมื่อคอมไพเลอร์สร้างเทมเพลต การยุบการอ้างอิงเป็นสาเหตุที่การอ้างอิงสากลใช้งานได้ แต่สมองของฉันไม่ชอบใส่สองคำในโดเมนเดียวกัน
aschepler
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.