Bug 106644 - [C++23] P2468R2 - The Equality Operator You Are Looking For
Summary: [C++23] P2468R2 - The Equality Operator You Are Looking For
Status: RESOLVED FIXED
Alias: None
Product: gcc
Classification: Unclassified
Component: c++ (show other bugs)
Version: unknown
: P3 normal
Target Milestone: 13.0
Assignee: Jason Merrill
URL:
Keywords:
Depends on:
Blocks: c++23-core
  Show dependency treegraph
 
Reported: 2022-08-16 16:27 UTC by Marek Polacek
Modified: 2022-11-28 22:15 UTC (History)
3 users (show)

See Also:
Host:
Target:
Build:
Known to work:
Known to fail:
Last reconfirmed: 2022-11-04 00:00:00


Attachments
gcc13-pr106644-wip.patch (2.50 KB, patch)
2022-10-25 11:34 UTC, Jakub Jelinek
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Marek Polacek 2022-08-16 16:27:17 UTC
See <https://wg21.link/p2468r2>.
Comment 1 Jakub Jelinek 2022-10-22 12:54:37 UTC
FYI, clang trunk now implements this DR.

Rough testcase distilled from the paper + some extra tests from the LLVM test coverage of the paper, with just // ERR
to mark expected diagnostics (details to be filled only once it is implemented).

// P2468R2 - The Equality Operator You Are Looking For
// { dg-do compile { target c++20 } }

struct A {
  bool operator== (const A &) { return true; }
  bool operator!= (const A &) { return false; }
};
bool a = A{} != A{};

template <typename T>
struct B {
  bool operator== (const T &) const;
  bool operator!= (const T &) const;
};
struct B1 : B<B1> { };
bool b1 = B1{} == B1{};
bool b2 = B1{} != B1{};

template <bool>
struct C {
  using C1 = C<true>;
  using C2 = C<false>;
  C () = default;
  C (const C2 &);
  bool operator== (C1) const;
  bool operator!= (C1) const;
};
using C3 = C<false>;
bool c = C3{} == C3{};

struct D {
  D ();
  D (int *);
  bool operator== (const D &) const;
  operator int * () const;
};
bool d = nullptr != D{};	// ERR

struct E {
  operator bool () const;
};
unsigned char operator== (E, E);
unsigned char e = E{} != E{};	// ERR

struct F {};
template <typename T>
bool operator== (F, T);
bool f1 = 0 == F ();
template <typename T>
bool operator!= (F, T);
bool f2 = 0 == F ();		// ERR
   
struct G {
  bool operator== (const G &);
};
struct G1 : G {
  G1 ();
  G1 (G);
  bool operator!= (const G &);
};
bool g1 = G () == G1 ();
bool g2 = G1 () == G ();	// ERR

struct H {};
template <typename T>
bool operator== (H, T);
inline namespace H1 {
  template <typename T>
  bool operator!= (H, T);
}
bool h = 0 == H ();

template <class T>
struct I {
  int operator== (const double &) const;
  friend inline int operator== (const double &, const T &) { return 1; }
};
struct I1 : I<I1> { };
bool i = I1{} == 0.;		// ERR

struct J {
  bool operator== (const J &) const;
  bool operator!= (const J &) const;
};
struct J1 : J {
  J1 (const J &);
  bool operator== (const J1 &x) const {
    return static_cast<const J &> (*this) == x;	// ERR
  }
};

struct K {
  bool operator== (const K &);
};
bool k = K{} == K{};		// ERR

struct L {
  bool operator== (const L &) const;
};
bool l = L{} == L{};

struct M {
  bool operator== (M);
};
bool m = M () == M ();

struct N {
  virtual bool operator== (const N &) const;
};
struct N1 : N {
  bool operator== (const N &) const override;
};
bool n = N1 () == N1 ();	// ERR

struct O {
  virtual signed char operator== (const O &) const;
  signed char operator!= (const O &x) const { return !operator== (x); }
};
struct O1 : O {
  signed char operator== (const O &) const override;
};
bool o = O1 () != O1 ();	// ERR

template <class T>
bool
foo (T x, T y)
requires requires { x == y; }
{
  return x == y;
}
bool b3 = foo (B1 (), B1 ());

struct P {};
template <typename T, class U = int>
bool operator== (P, T);
template <class T>
bool operator!= (P, T);
bool p = 0 == P ();

struct Q {};
template <typename T>
bool operator== (Q, T);
template <typename U>
bool operator!= (Q, U);
bool q = 0 == Q ();		// ERR

struct R {
  template <typename T>
  bool operator== (const T &);
};
bool r = R () == R ();		// ERR

struct S {
  template <typename T>
  bool operator== (const T &) const;
  bool operator!= (const S &);
};
bool s = S () == S ();

struct T {};
template <class U = int>
bool operator== (T, int);
bool operator!= (T, int);
bool t = 0 == T ();

struct U {};
bool operator== (U, int);
bool u1 = 0 == U ();
namespace U1 { bool operator!= (U, int); }
bool u2 = 0 == U ();
using U1::operator!=;
bool u3 = 0 == U ();		// ERR

struct V {};
template <typename T>
bool operator== (V, T);
bool v1 = 0 == V ();
namespace V1 { template <typename T> bool operator!= (V, T); }
bool v2 = 0 == V ();
using V1::operator!=;
bool v3 = 0 == V ();		// ERR

template <int N>
struct W {
bool operator== (int) requires (N == 1);
bool operator!= (int) requires (N == 2);
};
int w = 0 == W<1> ();

struct X { 
  bool operator== (X const &);
  static bool operator!= (X const &, X const &);
};
bool x = X () == X ();		// ERR
Comment 2 Jakub Jelinek 2022-10-22 12:57:42 UTC
The A-J cases are from the paper to be precise.
Comment 3 Jakub Jelinek 2022-10-22 13:11:08 UTC
As for implementation, I'd say in call.cc (add_candidates) we could do something like:
  if ((flags & (LOOKUP_REWRITTEN | LOOKUP_REVERSE))
      && DECL_OVERLOADED_OPERATOR_IS (fn, EQ_EXPR))
    {
    }

before the loop on the overloads and depending on whether fn is in class scope or namespace scope perform lookup (again argument dependent or not?) for
ovl_op_identifier (false, NE_EXPR) in the same scope.
But am not really sure what to do next if this finds something (supposedly in the loop over the overloads?); I suppose if no template is involved, compare the argument types (what else?), but for templates it is unclear (to me) if it should try to instantiate them and compare types only afterwards, or what exactly should be checked whether they correspond ([basic.scope.scope]).
Because e.g. http://eel.is/c++draft/basic.scope.scope#4.3.1 talks about equivalent trailing requires clauses etc.
Comment 4 Jakub Jelinek 2022-10-25 11:34:07 UTC
Created attachment 53770 [details]
gcc13-pr106644-wip.patch

Untested WIP.
Compared to vanilla trunk, this rejects the f2 case which is a2 from the example in the standard.  But doesn't handle anything else unfortunately.
In the g1 case (c1 from the example in the standard), it is accepted for a wrong reason, decls_match rejects the comparison between B::operator== and C::operator!= because they have different CP_DECL_CONTEXT and so we think that the operator== is a rewrite target when it is not.  Shall we use
something different than decls_match (say a copy of it that ignores the context
and for non-static members the this type too)?
The reason it is then accepted is some non-standard handling of the rewritten/reversed candidates in joust?
Then g2 case (c2 in the standard) is then incorrectly accepted because of that
code in joust or so.
Not really sure if we have a way to search just in a single namespace and not all the containing ones, and not sure about the using decls if those are something that should be ignored like in the patch or handled too.
The k case in the testcase is I think because of joust too.
The u3 case in the testcase is I think about whether using decls count or not.
Anyway, the remaining // ERR cases are where clang++ trunk rejects stuff but we with the patch don't.