非ブール戻り値との等価比較をオーバーロードすると、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でも失敗します。これを非Eigenの例にまだ削減できていません。これは、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;
}

式の2つの候補は次のとおりです。

  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)要素ごとの比較を行い、Arrayof を返すが含まれboolます。あなたはboolそれを他のものすべてを壊すことなくに変えることはできません。
TC

2
これは規格の欠陥のようです。書き換えのルールはoperator==既存のコードに影響を与えるとは考えられていませんでしたが、この場合、bool戻り値のチェックは書き換えの候補の選択の一部ではないため、既存のコードには影響します。
Nicol Bolas

2
@NicolBolas:一般的な原則、実装の変更が他のコードの解釈に影響を与えないようにするために、何かを実行できるかどうか(たとえば、演算子を呼び出すかどうか)をチェックすることです。書き直された比較は多くのことを壊しますが、ほとんどはすでに疑わしく、修正が簡単なものです。ですから、良くも悪くも、これらのルールはとにかく採用されました。
Davis Herring

うわー、ありがとうございます。あなたの解決策によって問題が解決されると思います(現時点では妥当な努力でgcc / clangトランクをインストールする時間がないので、これが最新の安定したコンパイラバージョンに問題がないかどうかを確認するだけです) )。
chtz

11

はい、コードは実際にはC ++ 20で壊れています。

表現 Foo{} != Foo{}は、C ++ 20には3つの候補があります(C ++ 17には1つしかありませんでした)。

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場合、iICSi(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、2番目のパラメーターの変換シーケンスは優れ#3ています(const追加のconst修飾を受ける必要があるパラメーターなので、変換シーケンスが悪くなります)。このconstフリップフロップにより、どちらか一方を優先できなくなります。

その結果、オーバーロードの解決全体はあいまいです。

私が理解している限り、これは戻り値の型がである場合にのみ機能し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勝ち#3、逆転されないことで勝つ-これは#1最良の実行可能な候補になります。C ++ 17で得られたのと同じ結果、そこにたどり着くためのほんの数ステップ。


オーバーロードの解決で戻り値の型を考慮しなければならないような状態になりたくありませんでした。」明確にするために、オーバーロードの解決自体では戻り値の型は考慮されませんが、後続の書き換えられた操作では考慮されます。オーバーロードの解決で書き換えが選択==され、選択された関数の戻り値の型がそうでない場合、コードの形式が正しくありませんbool。しかし、このカリングは、オーバーロードの解決自体の間は発生しません。
Nicol Bolas

実際には、戻り値の型が演算子をサポートしないものである場合にのみ不正な形式です!...
Chris Dodd

1
@ChrisDoddいいえ、それは正確でなければなりませんcv bool(この変更の前は、コンテキストへの変換boolでした-まだではありません!
バリー

残念ながら、これは私の実際の問題を解決しませんが、それは実際に自分の問題を説明するMREを提供できなかったためです。私はこれを受け入れ、問題を適切に軽減できるようになると、新しい質問をします...
chtz

2
元の問題の適切な削減はgcc.godbolt.org/z/tFy4qz
TC

5

[over.match.best] / 2は、セット内の有効なオーバーロードの優先順位を示しています。セクション2.8は、(他の多くのものの中で)F1以下のF2場合よりも優れていることを示しています。

F2書き換えられた候補([over.match.oper])でF1あり、

そこでの例は、明示的operator<に呼び出されていることを示していますoperator<=>

そして[over.match.oper] /3.4.3は、operator==、この状況におけるの候補が書き換えられた候補であることを示しています。

ただし、オペレーターは1つの重要なことを忘れconstます。それらは関数でなければなりません。そして、それらを作成しないconstことで、オーバーロード解決の初期の側面が機能するようになります。どちらの関数も、完全に一致するconstわけではありません。const異なる引数に対してへの変換を行う必要があるため。それが問題のあいまいさを引き起こしています。

それらを作成するとconstClangトランクがコンパイルされます

Eigenの他の部分と話すことはできません。コードがわからないため、コードが非常に大きく、MCVEに適合しません。


2
すべての引数に対して同等に優れた変換がある場合にのみ、リストしたタイブレーカーに到達します。しかし、それはありません。が欠落しているconstため、非反転候補は2番目の引数に対してより良い変換シーケンスを持ち、反転候補は最初の引数に対してより良い変換シーケンスを持っています。
Richard Smith

@RichardSmith:ええ、それは私が話していたような複雑さでした。しかし、私は実際にこれらのルールを読み、内部化する必要はありませんでした;)
Nicol Bolas

確かに、私constは最小限の例では忘れていました。Eigenがconstどこでも(またはクラス定義の外部で、const参照も使用して)使用することは確かですが、確認する必要があります。時間を見つけたとき、アイゲンが使用する全体的なメカニズムを最小限の例に分解しようとします。
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

これは実際には同様の問題ではありません。ニコルが指摘したように、これはすでにC ++ 17ではコンパイルできませんでした。別の理由で、C ++ 20ではコンパイルされません。
バリー

私は言及を忘れてしまった:我々はまた、メンバー演算子を提供します 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.