等式演算子で既存のコードを壊すC ++ 20の動作?

Jan 10 2021

この質問のデバッグ中にこれに遭遇しました。

ブースト演算子を使用するように切り詰めました:

  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ではコンパイルされない2行で無限再帰(最適化レベルに応じてクラッシュまたはタイトループ)が発生します。

明らかに、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)という1つの候補しかなかったので、それを選択します。その本体は、x == y会員候補(:、それ自体が唯一つの候補有する#1)暗黙的に変換することを含むy内をF。そして、そのメンバー候補は2つの整数メンバーを比較し、これはまったく問題ありません。

C ++ 20では、最初の式42 == F{42}2つの候補があります。#2以前のように非メンバー候補()と、逆メンバー候補(#1逆)の両方です。#2がより適切に一致します。変換を呼び出すのではなく、両方の引数を正確に一致させるため、選択されます。

ただし、x == y現在は2つの候補があります。メンバー候補(#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;
};