GCC9 หลีกเลี่ยงสถานะไร้ค่าของ std :: variant ที่อนุญาตหรือไม่


14

ฉันเพิ่งติดตามการสนทนา Reddit ซึ่งนำไปสู่การเปรียบเทียบที่ดีของstd::visitการเพิ่มประสิทธิภาพในคอมไพเลอร์ ฉันสังเกตเห็นสิ่งต่อไปนี้: https://godbolt.org/z/D2Q5ED

ทั้ง GCC9 และ Clang9 (ฉันเดาว่าพวกเขาแบ่งปัน stdlib เดียวกัน) ไม่ได้สร้างรหัสสำหรับการตรวจสอบและการโยนข้อยกเว้นที่ไม่มีค่าเมื่อทุกประเภทเป็นไปตามเงื่อนไขบางประการ สิ่งนี้นำไปสู่วิธีที่ดีกว่า codegen ดังนั้นฉันยกปัญหากับ MSVC STL และถูกนำเสนอด้วยรหัสนี้:

template <class T>
struct valueless_hack {
  struct tag {};
  operator T() const { throw tag{}; }
};

template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
  try { v.emplace<0>(valueless_hack<First>()); }
  catch(typename valueless_hack<First>::tag const&) {}
}

การอ้างสิทธิ์นี้ทำให้สิ่งต่าง ๆ ไร้ค่าและการอ่านเอกสารนั้นควร:

ก่อนอื่นให้ทำลายค่าที่มีอยู่ในปัจจุบัน (ถ้ามี) จากนั้นกำหนดค่าเริ่มต้นให้ตรงตามที่มีอยู่ราวกับว่าการสร้างค่าประเภทT_Iด้วยอาร์กิวเมนต์std::forward<Args>(args)....หากมีการโยนข้อยกเว้น*thisอาจกลายเป็น

สิ่งที่ฉันไม่เข้าใจ: เหตุใดจึงระบุว่า "อาจ" การอยู่ในสภาพเดิมถูกต้องตามกฎหมายหรือไม่หากการดำเนินการทั้งหมดเกิดขึ้น? เพราะนี่คือสิ่งที่ GCC ทำ:

  // For suitably-small, trivially copyable types we can create temporaries
  // on the stack and then memcpy them into place.
  template<typename _Tp>
    struct _Never_valueless_alt
    : __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
    { };

และในภายหลังมันก็มีเงื่อนไขดังนี้:

T tmp  = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);

ดังนั้นโดยทั่วไปจะสร้างชั่วคราวและหากประสบความสำเร็จคัดลอก / ย้ายมันเข้าไปในสถานที่จริง

IMO นี่เป็นการละเมิด "ขั้นแรกทำลายค่าที่มีอยู่ในปัจจุบัน" ตามที่ระบุไว้โดย docu เมื่อฉันอ่านมาตรฐานหลังจากนั้นv.emplace(...)ค่าปัจจุบันในชุดตัวเลือกจะถูกทำลายเสมอและประเภทใหม่เป็นประเภทชุดหรือไม่มีค่า

ฉันจะได้รับเงื่อนไขที่is_trivially_copyableไม่รวมทุกประเภทที่มีตัวทำลายที่สังเกตได้ ดังนั้นสิ่งนี้อาจเป็นได้ว่า: "ตัวแปร as-if ถูกกำหนดค่าเริ่มต้นใหม่ด้วยค่าเก่า" หรือดังนั้น แต่สถานะของตัวแปรนั้นเป็นผลที่สังเกตได้ ดังนั้นมาตรฐานจะอนุญาตอย่างแน่นอนซึ่งemplaceไม่เปลี่ยนค่าปัจจุบันหรือไม่

แก้ไขตามคำพูดมาตรฐาน:

จากนั้นเริ่มต้นค่าที่มีอยู่เช่นถ้าตรงที่ไม่ใช่รายการเริ่มต้นค่าของประเภท TI std​::​forward<Args>(args)...กับข้อโต้แย้ง

ไม่T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);จริงๆนับเป็นการดำเนินงานที่ถูกต้องดังกล่าวข้างต้น? นี่คือสิ่งที่มีความหมายโดย "ราวกับว่า"?

คำตอบ:


7

ฉันคิดว่าส่วนสำคัญของมาตรฐานคือ:

จากhttps://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4 Modi fi ers

( ... )

แม่แบบ variant_alternative_t> & emplace (Args && ... args);

(... ) หากมีข้อผิดพลาดเกิดขึ้นระหว่างการเริ่มต้นของค่าที่มีอยู่ตัวแปรอาจไม่เก็บค่าไว้

มันบอกว่า "อาจ" ไม่ "ต้อง" ฉันคาดหวังว่าสิ่งนี้จะเป็นการจงใจเพื่ออนุญาตให้มีการใช้งานเช่นที่ใช้โดย gcc

ดังที่คุณพูดถึงตัวคุณเองสิ่งนี้เป็นไปได้ก็ต่อเมื่อตัวทำลายของทางเลือกทั้งหมดนั้นไม่สำคัญและไม่สามารถสังเกตเห็นได้เพราะทำลายค่าก่อนหน้านี้

คำถามติดตาม

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

T tmp {std :: forward (args) ... } หรือไม่ this-> value = std :: move (tmp); จริง ๆ นับเป็นการใช้งานที่ถูกต้องของข้างต้นหรือไม่ นี่คือสิ่งที่มีความหมายโดย "ราวกับว่า"?

ใช่เพราะสำหรับประเภทที่คัดลอกได้เล็กน้อยนั้นไม่มีวิธีการตรวจสอบความแตกต่างดังนั้นการนำไปปฏิบัติจะมีลักษณะราวกับว่าค่าเริ่มต้นตามที่อธิบายไว้ สิ่งนี้จะไม่ทำงานหากประเภทไม่สามารถคัดลอกได้เล็กน้อย


น่าสนใจ ฉันอัปเดตคำถามด้วยคำขอติดตาม / คำชี้แจง รูทคือ: อนุญาตให้คัดลอก / ย้ายได้หรือไม่ ฉันสับสนมากโดยmight/mayถ้อยคำเนื่องจากมาตรฐานไม่ได้ระบุว่าทางเลือกคืออะไร
Flamefire

there is no way to detect the differenceการยอมรับนี้ได้อ้างมาตรฐานและ
Flamefire

5

ดังนั้นมาตรฐานจะอนุญาตอย่างแน่นอนซึ่งemplaceไม่เปลี่ยนค่าปัจจุบันหรือไม่

ใช่. emplaceจะต้องให้การรับประกันขั้นพื้นฐานโดยไม่มีการรั่วไหล (เช่นการเคารพอายุการใช้งานของวัตถุเมื่อการก่อสร้างและการทำลายเกิดผลข้างเคียงที่สังเกตได้) แต่เมื่อเป็นไปได้จะได้รับอนุญาตให้รับประกันที่แข็งแกร่ง (เช่นสถานะเดิม

variantจะต้องทำตัวคล้ายกับสหภาพ - ทางเลือกได้รับการจัดสรรในพื้นที่หนึ่งของการจัดเก็บที่จัดสรรอย่างเหมาะสม ไม่อนุญาตให้จัดสรรหน่วยความจำแบบไดนามิก ดังนั้นการเปลี่ยนชนิดemplaceจึงไม่มีวิธีในการเก็บรักษาวัตถุต้นฉบับโดยไม่เรียกตัวสร้างการย้ายเพิ่มเติม - มันจะต้องทำลายมันและสร้างวัตถุใหม่แทนที่มัน หากการก่อสร้างนี้ล้มเหลวตัวแปรจะต้องไปยังสถานะไร้ค่าพิเศษ สิ่งนี้จะป้องกันสิ่งแปลก ๆ เช่นการทำลายวัตถุที่ไม่มีอยู่

อย่างไรก็ตามสำหรับประเภทที่คัดลอกได้เล็กน้อยมีความเป็นไปได้ที่จะรับประกันที่ดีโดยไม่มีค่าใช้จ่ายมากเกินไป (แม้แต่การเพิ่มประสิทธิภาพในการหลีกเลี่ยงการตรวจสอบในกรณีนี้) ดังนั้นการดำเนินการมัน นี่คือการปฏิบัติตามมาตรฐาน: การใช้งานยังคงให้การรับประกันพื้นฐานตามที่กำหนดโดยมาตรฐานในลักษณะที่เป็นมิตรต่อผู้ใช้มากขึ้น

แก้ไขตามคำพูดมาตรฐาน:

จากนั้นเริ่มต้นค่าที่มีอยู่เช่นถ้าตรงที่ไม่ใช่รายการเริ่มต้นค่าของประเภท TI std​::​forward<Args>(args)...กับข้อโต้แย้ง

ไม่T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);จริงๆนับเป็นการดำเนินงานที่ถูกต้องดังกล่าวข้างต้น? นี่คือสิ่งที่มีความหมายโดย "ราวกับว่า"?

ใช่ถ้าการมอบหมายการย้ายไม่มีผลที่สังเกตได้ซึ่งเป็นกรณีของประเภทที่คัดลอกได้เล็กน้อย


ฉันเห็นด้วยกับเหตุผลเชิงตรรกะอย่างเต็มที่ ฉันแค่ไม่แน่ใจว่านี่เป็นมาตรฐานจริงหรือ คุณสำรองข้อมูลนี้ไว้กับอะไรก็ได้?
Flamefire

@Flamefire อืม ... โดยทั่วไปฟังก์ชั่นมาตรฐานให้การรับประกันขั้นพื้นฐาน (เว้นแต่มีบางอย่างผิดปกติกับสิ่งที่ผู้ใช้ให้) และstd::variantไม่มีเหตุผลที่จะทำลายมัน ฉันยอมรับว่าสิ่งนี้สามารถทำให้ชัดเจนยิ่งขึ้นในถ้อยคำของมาตรฐาน แต่นี่คือสิ่งที่คนอื่นเป็นส่วนหนึ่งของงานห้องสมุดมาตรฐาน และ FYI, P0088เป็นข้อเสนอเริ่มต้น
LF

ขอบคุณ มีข้อกำหนดที่ชัดเจนมากขึ้นภายใน: if an exception is thrown during the call toT’s constructor, valid()will be false;ดังนั้นจึงห้าม "การเพิ่มประสิทธิภาพ" นี้
Flamefire

ใช่. ข้อมูลจำเพาะของemplaceใน P0088 ภายใต้Exception safety
Flamefire

@Flamefire ดูเหมือนว่าจะมีความแตกต่างระหว่างข้อเสนอเดิมและรุ่นที่ได้รับการโหวตรุ่นสุดท้ายเปลี่ยนเป็นคำว่า "อาจ"
LF
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.