같음 연산자로 기존 코드를 깨는 C ++ 20 동작?

Jan 10 2021

이 질문 을 디버깅하는 동안 이 문제가 발생했습니다 .

Boost Operators를 사용하기 위해 끝까지 정리했습니다 .

  1. 컴파일러 탐색기 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
    }
    

    이 프로그램은 GCC와 Clang 모두에서 C ++ 17 (ubsan / asan 활성화 됨)로 잘 컴파일되고 실행됩니다.

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