1. ホーム
  2. c++

[解決済み] C++20の動作は、等号演算子で既存のコードを壊すか?

2022-10-04 17:51:12

質問

デバッグ中に以下のようなことに遭遇しました。 この質問 .

私はそれをわざわざ切り詰めて、単に ブースト演算子 :

  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の新しい"spaceship operator"のサポートのためかもしれないと思うが、理解できない。 はどのように がこのコードの振る舞いを変えるのか理解できません。

どのように解決するのですか?

確かに、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 では、候補は 1 つだけでした。非メンバーの候補 ( #2 ) という 1 つの候補しかなかったので、それを選択します。その本体である x == y は、それ自体が1つの候補を持つだけです:メンバー候補 ( #1 ) で、これは暗黙のうちに yF . そして、そのメンバー候補は2つの整数のメンバーを比較し、これは全く問題ありません。

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