C ++ 20 พฤติกรรมทำลายรหัสที่มีอยู่ด้วยตัวดำเนินการความเท่าเทียมกัน?

Jan 10 2021

ฉันพบปัญหานี้ในขณะที่แก้ไขข้อบกพร่องของคำถามนี้

ฉันตัดมันลงจนสุดเพื่อใช้Boost Operators :

  1. คอมไพเลอร์ Explorer C ++ 17 C ++ 20

    #include <boost/operators.hpp>
    
    struct F : boost::totally_ordered1<F, boost::totally_ordered2<F, int>> {
        /*implicit*/ F(int t_) : t(t_) {}
        bool operator==(F const& o) const { return t == o.t; }
        bool operator< (F const& o) const { return t <  o.t; }
      private: int t;
    };
    
    int main() {
        #pragma GCC diagnostic ignored "-Wunused"
        F { 42 } == F{ 42 }; // OKAY
        42 == F{42};         // C++17 OK, C++20 infinite recursion
        F { 42 } == 42;      // C++17 OK, C++20 infinite recursion
    }
    

    โปรแกรมนี้รวบรวมและทำงานได้ดีด้วย C ++ 17 (เปิดใช้งาน ubsan / asan) ทั้งใน GCC และ Clang

  2. เมื่อคุณเปลี่ยนตัวสร้างโดยนัยexplicitเป็นบรรทัดที่มีปัญหาจะไม่คอมไพล์บน C ++ 17 อีกต่อไป

น่าแปลกใจที่ทั้งสองเวอร์ชันคอมไพล์บน C ++ 20 ( v1และv2 )แต่นำไปสู่การเรียกซ้ำแบบไม่มีที่สิ้นสุด (ความผิดพลาดหรือการวนซ้ำขึ้นอยู่กับระดับการปรับให้เหมาะสม) บนสองบรรทัดที่จะไม่รวบรวมใน C ++ 17

เห็นได้ชัดว่าข้อผิดพลาดเงียบประเภทนี้กำลังคืบคลานเข้ามาโดยการอัปเกรดเป็น C ++ 20 นั้นน่าเป็นห่วง

คำถาม:

  • เป็นไปตามลักษณะการทำงานของ c ++ 20 หรือไม่ (ฉันคาดหวังเช่นนั้น)
  • รบกวนอะไรกันแน่? ฉันสงสัยว่าอาจเป็นเพราะการสนับสนุน "ยานอวกาศ" ใหม่ของ c ++ 20 แต่ไม่เข้าใจว่ามันเปลี่ยนพฤติกรรมของโค้ดนี้อย่างไร

คำตอบ

81 Barry Jan 10 2021 at 07:27

อันที่จริง C ++ 20 น่าเสียดายที่ทำให้รหัสนี้วนซ้ำไม่สิ้นสุด

นี่คือตัวอย่างที่ลดลง:

struct F {
    /*implicit*/ F(int t_) : t(t_) {}

    // member: #1
    bool operator==(F const& o) const { return t == o.t; }

    // non-member: #2
    friend bool operator==(const int& y, const F& x) { return x == y; }

private:
    int t;
};

42 == F{42}ให้ดูเพียงแค่

ใน C ++ 17 เรามีผู้สมัครเพียงคนเดียว: ผู้สมัครที่ไม่ใช่สมาชิก ( #2) ดังนั้นเราจึงเลือกสิ่งนั้น ตัวของมันx == yเองมีผู้สมัครเพียงคนเดียว: ผู้สมัครสมาชิก ( #1) ซึ่งเกี่ยวข้องกับการแปลงโดยปริยายyเป็นไฟล์F. จากนั้นผู้สมัครสมาชิกนั้นจะเปรียบเทียบสมาชิกจำนวนเต็มสองตัวและนี่ก็ใช้ได้โดยสิ้นเชิง

ใน C ++ 20 นิพจน์เริ่มต้น42 == F{42}มีผู้สมัครสองคน: ทั้งผู้สมัครที่ไม่ใช่สมาชิก ( #2) เหมือนก่อนหน้านี้และตอนนี้ยังเป็นผู้สมัครสมาชิกที่#1กลับรายการ( ย้อนกลับ) #2เป็นการจับคู่ที่ดีกว่า - เราจับคู่อาร์กิวเมนต์ทั้งสองแทนการเรียกใช้การแปลงดังนั้นจึงถูกเลือก

อย่างไรก็ตามx == yตอนนี้มีผู้สมัครสองคน: ผู้สมัครสมาชิกอีกครั้ง ( #1) แต่ยังมีผู้สมัครที่ไม่ได้เป็นสมาชิกที่#2กลับรายการด้วย( ย้อนกลับ) #2เป็นการจับคู่ที่ดีกว่าอีกครั้งด้วยเหตุผลเดียวกับที่เคยเป็นคู่ที่ดีกว่าก่อนหน้านี้: ไม่จำเป็นต้องมีการแปลง ดังนั้นเราจึงประเมินy == xแทน การเรียกซ้ำที่ไม่มีที่สิ้นสุด

ผู้สมัครที่ไม่กลับรายการเป็นที่ต้องการในการกลับตัวของผู้สมัคร แต่เป็นเพียงตัวผูก ลำดับการแปลงที่ดีขึ้นเป็นอันดับแรกเสมอ


โอเคดีเราจะแก้ไขได้อย่างไร ตัวเลือกที่ง่ายที่สุดคือการลบผู้สมัครที่ไม่ใช่สมาชิกทั้งหมด:

struct F {
    /*implicit*/ F(int t_) : t(t_) {}

    bool operator==(F const& o) const { return t == o.t; }

private:
    int t;
};

42 == F{42}ที่นี่ประเมินว่าF{42}.operator==(42)ซึ่งใช้งานได้ดี

หากเราต้องการเก็บผู้สมัครที่ไม่ใช่สมาชิกไว้เราสามารถเพิ่มผู้สมัครที่กลับรายการได้อย่างชัดเจน:

struct F {
    /*implicit*/ F(int t_) : t(t_) {}
    bool operator==(F const& o) const { return t == o.t; }
    bool operator==(int i) const { return t == i; }
    friend bool operator==(const int& y, const F& x) { return x == y; }

private:
    int t;
};

สิ่งนี้ทำให้42 == F{42}ยังคงเลือกผู้สมัครที่ไม่ใช่สมาชิก แต่ตอนนี้x == yในร่างกายจะชอบผู้สมัครสมาชิกซึ่งจะทำให้เกิดความเท่าเทียมกันตามปกติ

เวอร์ชันสุดท้ายนี้ยังสามารถลบผู้สมัครที่ไม่ใช่สมาชิกได้ สิ่งต่อไปนี้ยังใช้งานได้โดยไม่มีการเรียกซ้ำสำหรับกรณีทดสอบทั้งหมด (และเป็นวิธีที่ฉันจะเขียนการเปรียบเทียบใน C ++ 20 ต่อไป):

struct F {
    /*implicit*/ F(int t_) : t(t_) {}
    bool operator==(F const& o) const { return t == o.t; }
    bool operator==(int i) const { return t == i; }

private:
    int t;
};