Hành vi C ++ 20 phá vỡ mã hiện có với toán tử bình đẳng?

Jan 10 2021

Tôi đã gặp phải vấn đề này trong khi gỡ lỗi câu hỏi này .

Tôi đã cắt nó xuống bằng mọi cách để chỉ sử dụng các Toán tử Boost :

  1. Trình biên dịch 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
    }
    

    Chương trình này biên dịch và chạy tốt với C ++ 17 (đã bật ubsan / asan) trong cả GCC và Clang.

  2. Khi bạn thay đổi hàm tạo ngầm định thành explicit, các dòng có vấn đề rõ ràng không còn được biên dịch trên C ++ 17 nữa

Đáng ngạc nhiên là cả hai phiên bản đều biên dịch trên C ++ 20 ( v1 và v2 ) , nhưng chúng dẫn đến đệ quy vô hạn (sự cố hoặc vòng lặp chặt chẽ, tùy thuộc vào mức độ tối ưu hóa) trên hai dòng không biên dịch trên C ++ 17.

Rõ ràng là loại lỗi âm thầm len lỏi vào khi nâng cấp lên C ++ 20 là điều đáng lo ngại.

Câu hỏi:

  • Đây có phải là hành vi tuân thủ c ++ 20 không (tôi mong đợi như vậy)
  • Chính xác thì can thiệp là gì? Tôi nghi ngờ nó có thể là do hỗ trợ "toán tử phi thuyền" mới của c ++ 20, nhưng không hiểu nó thay đổi hành vi của mã này như thế nào .

Trả lời

81 Barry Jan 10 2021 at 07:27

Thật vậy, thật không may, C ++ 20 làm cho mã này trở nên đệ quy vô hạn.

Đây là một ví dụ rút gọn:

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

Hãy chỉ nhìn vào 42 == F{42}.

Trong C ++ 17, chúng tôi chỉ có một ứng cử viên: ứng viên không phải thành viên ( #2), vì vậy chúng tôi chọn nó. Phần thân của nó x == y, bản thân nó chỉ có một ứng cử viên: ứng viên thành viên ( #1) liên quan đến việc chuyển đổi ngầm ythành một F. Và sau đó ứng viên thành viên đó so sánh hai thành viên nguyên và điều này hoàn toàn ổn.

Trong C ++ 20, biểu thức ban đầu 42 == F{42}bây giờ có hai ứng cử viên: cả ứng viên không phải thành viên ( #2) như trước và bây giờ cũng là ứng viên thành viên được #1đảo ngược ( đã đảo ngược). #2là đối sánh tốt hơn - chúng tôi đối sánh chính xác cả hai đối số thay vì gọi một chuyển đổi, vì vậy nó được chọn.

Tuy nhiên, x == ybây giờ có hai ứng cử viên: ứng cử viên thành viên lại ( #1), và ứng viên không phải là thành viên #2đã đảo ngược ( đã đảo ngược). #2là đối sánh tốt hơn một lần nữa vì cùng một lý do rằng trước đây là đối sánh tốt hơn: không cần chuyển đổi. Vì vậy, chúng tôi đánh giá y == xthay thế. Đệ quy vô hạn.

Các ứng cử viên không bị đảo ngược được ưu tiên hơn các ứng viên bị đảo ngược, nhưng chỉ như một yếu tố quyết định. Trình tự chuyển đổi tốt hơn luôn là đầu tiên.


Được rồi, tuyệt vời, chúng ta có thể sửa nó như thế nào? Tùy chọn đơn giản nhất là loại bỏ hoàn toàn ứng viên không phải là thành viên:

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

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

private:
    int t;
};

42 == F{42}ở đây đánh giá là F{42}.operator==(42), hoạt động tốt.

Nếu chúng tôi muốn giữ lại ứng viên không phải là thành viên, chúng tôi có thể thêm ứng viên đã đảo ngược của nó một cách rõ ràng:

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

Điều này làm cho 42 == F{42}vẫn chọn ứng cử viên không phải thành viên, nhưng bây giờ x == ytrong cơ thể sẽ thích ứng viên thành viên hơn, điều này sau đó thực hiện bình đẳng thông thường.

Phiên bản cuối cùng này cũng có thể xóa ứng cử viên không phải là thành viên. Phần sau cũng hoạt động mà không cần đệ quy cho tất cả các trường hợp thử nghiệm (và là cách tôi sẽ viết so sánh trong C ++ 20 về sau):

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