부울이 아닌 반환 값과 동등 비교를 오버로드 할 때 C ++ 20의 변경 사항 또는 clang-trunk / gcc-trunk의 회귀가 변경됩니까?


11

다음 코드는 c ++ 17 모드에서 clang-trunk로 잘 컴파일되지만 c ++ 2a (다가오는 c ++ 20) 모드에서 중단됩니다.

// Meta struct describing the result of a comparison
struct Meta {};

struct Foo {
    Meta operator==(const Foo&) {return Meta{};}
    Meta operator!=(const Foo&) {return Meta{};}
};

int main()
{
    Meta res = (Foo{} != Foo{});
}

또한 gcc-trunk 또는 clang-9.0.0으로 잘 컴파일됩니다 : https://godbolt.org/z/8GGT78

clang-trunk와의 오류 -std=c++2a:

<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
    Meta res = (f != g);
                ~ ^  ~
<source>:6:10: note: candidate function
    Meta operator!=(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function
    Meta operator==(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function (with reversed parameter order)

C ++ 20은 과부하 만 가능하게 operator==하고 컴파일러는 operator!=의 결과를 무시하여 자동으로 생성 한다는 것을 알고 있습니다 operator==. 내가 이해하는 한, 이것은 반환 유형이 인 한 작동 bool합니다.

문제의 원인은 고유치 우리 사업자들의 세트를 선언하는 것입니다 ==, !=, <사이 ... Array물체 나 Array및 스칼라 창 (식의) 배열 bool다음 요소 와이즈 접속되거나, 그렇지 않으면 사용될 수있다 ( ). 예 :

#include <Eigen/Core>
int main()
{
  Eigen::ArrayXd a(10);
  a.setRandom();
  return (a != 0.0).any();
}

위의 예제와 달리 gcc-trunk에서도 실패합니다 : https://godbolt.org/z/RWktKs . clang-trunk와 gcc-trunk 모두에서 실패하는 고유하지 않은 예제로 이것을 줄이기 위해 아직 관리하지 않았습니다 (맨 위의 예제는 매우 간단합니다).

관련 문제 보고서 : https://gitlab.com/libeigen/eigen/issues/1833

내 실제 질문 : 이것은 실제로 C ++ 20의 주요 변경 사항입니까 (그리고 메타 연산자를 반환하기 위해 비교 연산자를 오버로드 할 가능성이 있습니까) 아니면 clang / gcc에서 회귀 가능성이 있습니까?


답변:


5

고유 문제는 다음과 같이 감소합니다.

using Scalar = double;

template<class Derived>
struct Base {
    friend inline int operator==(const Scalar&, const Derived&) { return 1; }
    int operator!=(const Scalar&) const;
};

struct X : Base<X> {};

int main() {
    X{} != 0.0;
}

표현의 두 후보는

  1. 다시 쓴 후보 operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

당 [over.match.funcs / 4 등 operator!=의 범위로 반입하지 않은 X부산물 사용 선언 , # 2의 내장 객체 파라미터의 타입이다 const Base<X>&. 결과적으로 # 1은 해당 인수에 대해 더 나은 암시 적 변환 순서를 갖습니다 (파생-대-변환보다는 정확한 일치). # 1을 선택하면 프로그램이 잘못 작성됩니다.

가능한 수정 사항 :

  • 추가 using Base::operator!=;하기 위해 Derived, 또는
  • (가) 변경 operator==테이크 할 const Base&대신을 const Derived&.

실제 코드가를 반환 할 수없는 이유 거기에 bool자신의에서는 operator==? 그것이 새로운 규칙에 따라 코드가 잘못 작성된 유일한 이유 인 것 같습니다.
— Nicol Bolas

4
실제 코드는 포함 operator==(Array, Scalar)요소 현명한 비교를 수행하고 돌아 그 Array의를 bool. bool다른 모든 것을 깨지 않고 는 그것을 바꿀 수 없습니다 .
— TC

2
이것은 표준의 결함과 비슷합니다. 다시 쓰기 규칙은 operator==기존 코드에 영향을 미치지 않았지만이 경우에는 bool반환 값 확인 이 다시 쓰기 후보를 선택하는 것이 아니기 때문에 적용 됩니다.
— Nicol Bolas

2
@NicolBolas : 준수해야 할 일반적인 원칙은 구현 변경이 다른 코드의 해석에 자동으로 영향을 미치지 않도록 해야 할 것이 아니라 수행 할 수 있는 것이 아니라 무엇을 할 수 있는지 ( 예 : 연산자 호출) 여부를 확인하는 것 입니다. 재 작성된 비교는 많은 것들을 망가 뜨리지 만, 대부분 이미 의심스럽고 수정하기 쉬운 것들로 밝혀졌습니다. 따라서 더 좋거나 나쁘게도 이러한 규칙은 어쨌든 채택되었습니다.
— Davis Herring

와우, 정말 고마워, 귀하의 솔루션으로 문제를 해결할 것으로 생각합니다 (현재 합리적인 노력으로 gcc / clang 트렁크를 설치할 시간이 없으므로 최신의 안정적인 컴파일러 버전으로 업그레이드되는지 확인하십시오. ).
— chtz

11

예, 실제로 코드는 C ++ 20에서 중단됩니다.

이 표현식 Foo{} != Foo{}에는 C ++ 20에 세 개의 후보가 있습니다 (C ++ 17에는 하나만있는 경우).

Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed

이것은 [over.match.oper] /3.4 의 새로운 재 작성 후보 규칙에서 비롯된 것 입니다. 우리의 Foo주장은 그렇지 않기 때문에 모든 후보자들은 실행 가능 const합니다. 가장 적합한 후보를 찾으려면 순위 결정자를 통과해야합니다.

최상의 실행 기능에 대한 관련 규칙은 [over.match.best] / 2입니다 .

이러한 정의 감안할 때, 실행 가능한 기능은 F1다른 가능한 기능보다 더 나은 기능으로 정의 된 F2모든 인수를위한 경우 i, 보다 더 나쁜 변환 순서가 아닌 다음 ICSi(F1)ICSi(F2)

  • [...이 예와 관련이없는 많은 경우 ...] 또는 그렇지 않은 경우
  • F2는 다시 작성된 후보 ([over.match.oper])이며 F1은 그렇지 않습니다.
  • F1과 F2는 다시 작성된 후보이고, F2는 반대 순서의 매개 변수를 가진 합성 된 후보이며 F1은 그렇지 않습니다.

#2및 #3후보 재 기입하고, #3상태 파라미터의 순서를 역전하고있다 #1재기록되지 않는다. 그러나 그 계급 자에 도달하려면 먼저 그 초기 조건을 통과해야합니다. 모든 인수에 대해 변환 순서가 나쁘지 않습니다.

#1#2모든 변환 시퀀스가 ​​동일하고 (사소하게는 함수 매개 변수가 동일하기 때문에) #2재 작성 후보이지만 #1그렇지 않은 것보다 낫습니다 .

그러나 ... 두 쌍 #1/ #3및 #2/ #3 는 그 첫 번째 조건에 붙어 있습니다. 두 경우 모두 첫 번째 매개 변수는 #1/에 대해 더 나은 변환 순서를 #2갖지만 두 번째 매개 변수는 더 나은 변환 순서를 갖습니다 (추가 검증 을 받아야하는 #3매개 변수 이므로 변환 순서가 더 나쁩니다). 이 플립 플롭은 우리가 둘 중 하나를 선호하지 못하게합니다.constconstconst

결과적으로 전체 과부하 해상도가 모호합니다.

내가 이해하는 한, 이것은 반환 유형이 인 한 작동 bool합니다.

맞지 않습니다. 우리는 무조건 재 작성 및 역 후보 후보자를 고려합니다. 우리가 가진 규칙은 [over.match.oper] / 9입니다 .

operator==운영자에 대해 과부하 해결을 통해 재 작성된 후보자를 선정 @하면 그 반환 유형은 cv가됩니다. bool

즉, 우리는 여전히 이러한 후보들을 고려합니다. 그러나 가장 실행 가능한 후보가 다음과 같은 결과 operator==를 반환 Meta하는 경우-결과는 기본적으로 해당 후보가 삭제 된 것과 동일합니다.

과부하 해결이 리턴 유형을 고려해야하는 상태 가 아니 었습니다 . 그리고 어쨌든 여기에서 코드가 반환한다는 사실 Meta은 중요하지 않습니다 bool. 반환 된 경우에도 문제가 발생합니다 .


고맙게도 여기의 수정은 쉽습니다.

struct Foo {
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
};

두 개의 비교 연산자를 작성하면 const더 이상 모호성이 없습니다. 모든 매개 변수가 동일하므로 모든 변환 시퀀스가 ​​거의 동일합니다. #1이제는 #3다시 쓰지 않고 이길 것이고 , 뒤집 히지 않으면 서 #2이길 것 입니다. C ++ 17에서와 동일한 결과를 얻을 수있는 몇 가지 단계가 더 있습니다.#3#1


" 우리는 과부하 해결이 리턴 유형을 고려해야하는 상태에 있지 않기를 원했습니다. "오버로드 해결 자체는 리턴 유형을 고려하지 않지만 후속 재 작성 작업은 명확 합니다. 과부하 해결이 재 작성을 선택 ==하고 선택한 함수의 반환 유형이 아닌 경우 코드가 잘못 구성됩니다 bool. 그러나이 컬링은 과부하 해결 자체에는 발생하지 않습니다.
— Nicol Bolas

반환 유형이 연산자를 지원하지 않는 경우에만 실제로 잘못 형성됩니다 ....
— Chris Dodd

1
@ChrisDodd 아니요, 정확하게 cv bool변경 되어야합니다 (이 변경 전에 요구 사항은 상황에 맞는 상황으로 전환 bool되지 않았습니다 !)
— Barry

불행히도 이것은 실제 문제를 해결하지 못하지만 실제로 문제를 설명하는 MRE를 제공하지 못했기 때문입니다. 나는 이것을 받아들이고 내 문제를 제대로 줄일 수있을 때 새로운 질문을 할 것이다 ...
— chtz

2
원래 문제에 대한 적절한 축소 방법은 다음과 같습니다. gcc.godbolt.org/z/tFy4qz
— TC

5

[over.match.best] / 2는 세트의 유효한 과부하가 우선 순위를 매기는 방법을 나열합니다. 섹션 2.8 은 ( 다른 많은 것들 중에서) if F1보다 낫다 는 것을 알려줍니다 .F2

F2재 작성된 후보 ([over.match.oper])을하고 F1아니다

이 예제는 operator<비록 명시 적이라고 부르는 것을 보여줍니다 operator<=>.

그리고 [over.match.oper] /3.4.3operator== 은이 상황에서 후보자가 다시 작성된 후보 라고 알려줍니다 .

그러나 운영자는 한 가지 중요한 사실을 잊어 버립니다. 즉 const기능 이어야합니다 . 그리고 그것들을 만들지 않으면 const과부하 해결의 초기 측면이 작동 하지 않습니다 . 다른 인수에 대해 비- const대- const변환이 발생해야하기 때문에 두 함수 모두 정확히 일치하는 것은 아닙니다 . 그로 인해 문제가 모호해집니다.

당신이 그들을하게되면 const, 연타는 컴파일을 트렁크 .

코드를 모르기 때문에 나머지 Eigen과 대화 할 수 없으며 코드가 매우 커서 MCVE에 맞지 않습니다.


2
모든 인수에 대해 동일한 전환이있는 경우에만 순위를 매기십시오. 누락되지 않았기 때문에 const반전되지 않은 후보는 두 번째 인수에 대해 더 나은 변환 시퀀스를 가지며 반전 된 후보는 첫 번째 인수에 대해 더 나은 변환 시퀀스를 갖지 않습니다.
— Richard Smith

@RichardSmith : 예, 그것은 제가 이야기 한 복잡한 종류였습니다. 그러나 나는 실제로 그 규칙들을 읽고 읽고 / 내장 화하고 싶지는 않았다;)
— Nicol Bolas

실제로, 나는 const최소한의 예에서 잊어 버렸습니다 . 나는 Eigen이 const모든 곳에서 (또는 클래스 정의 외부, const참조를 사용하여) 사용한다고 확신 하지만 확인해야합니다. 나는 시간을 찾을 때 Eigen이 사용하는 전체 메커니즘을 최소한의 예로 분해하려고합니다.
— chtz

-1

Goopax 헤더 파일과 비슷한 문제가 있습니다. clang-10 및 -std = c ++ 2a로 다음을 컴파일하면 컴파일러 오류가 발생합니다.

template<typename T> class gpu_type;

using gpu_bool     = gpu_type<bool>;
using gpu_int      = gpu_type<int>;

template<typename T>
class gpu_type
{
  friend inline gpu_bool operator==(T a, const gpu_type& b);
  friend inline gpu_bool operator!=(T a, const gpu_type& b);
};

int main()
{
  gpu_int a;
  gpu_bool b = (a == 0);
}

이러한 추가 연산자를 제공하면 문제가 해결되는 것 같습니다.

template<typename T>
class gpu_type
{
  ...
  friend inline gpu_bool operator==(const gpu_type& b, T a);
  friend inline gpu_bool operator!=(const gpu_type& b, T a);
};

1
이것은 미리 수행했을 때 유용한 것이 아니 었습니까? 그렇지 않으면 어떻게 a == 0컴파일했을 까요?
— Nicol Bolas

이것은 실제로 비슷한 문제가 아닙니다. Nicol이 지적했듯이 이것은 C ++ 17에서 이미 컴파일되지 않았습니다. C ++ 20에서는 계속 다른 이유로 컴파일되지 않습니다.
— Barry

나는 얘기를 깜빡 했네요 : 우리는 또한 회원 연산자를 제공 gpu_bool gpu_type<T>::operator==(T a) const;하고 gpu_bool gpu_type<T>::operator!=(T a) const;와 C ++ - (17)이 잘 작동합니다. 그러나 이제 clang-10 및 C ++-20에서는 더 이상 찾을 수 없으며 대신 컴파일러는 인수를 교체하여 자체 연산자를 생성하려고 시도하지만 반환 유형이 아니기 때문에 실패합니다 bool.
— Ingo Josopait
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.