diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -356,6 +356,7 @@ the time of checking, which should now allow the libstdc++ ranges implementation to work for at least trivial examples. This fixes `Issue 44178 `_. +- Implemented The Equality Operator You Are Looking For (`P2468 `_). C++2b Feature Support ^^^^^^^^^^^^^^^^^^^^^ diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -4701,6 +4701,8 @@ def note_ovl_ambiguous_oper_binary_reversed_self : Note< "ambiguity is between a regular call to this operator and a call with the " "argument order reversed">; +def note_ovl_ambiguous_eqeq_reversed_self_non_const : Note< + "mark operator== as const or add a matching operator!= to resolve the ambiguity">; def note_ovl_ambiguous_oper_binary_selected_candidate : Note< "candidate function with non-reversed arguments">; def note_ovl_ambiguous_oper_binary_reversed_candidate : Note< diff --git a/clang/include/clang/Sema/Overload.h b/clang/include/clang/Sema/Overload.h --- a/clang/include/clang/Sema/Overload.h +++ b/clang/include/clang/Sema/Overload.h @@ -966,12 +966,16 @@ /// functions to a candidate set. struct OperatorRewriteInfo { OperatorRewriteInfo() - : OriginalOperator(OO_None), AllowRewrittenCandidates(false) {} - OperatorRewriteInfo(OverloadedOperatorKind Op, bool AllowRewritten) - : OriginalOperator(Op), AllowRewrittenCandidates(AllowRewritten) {} + : OriginalOperator(OO_None), OpLoc(), AllowRewrittenCandidates(false) {} + OperatorRewriteInfo(OverloadedOperatorKind Op, SourceLocation OpLoc, + bool AllowRewritten) + : OriginalOperator(Op), OpLoc(OpLoc), + AllowRewrittenCandidates(AllowRewritten) {} /// The original operator as written in the source. OverloadedOperatorKind OriginalOperator; + /// The source location of the operator. + SourceLocation OpLoc; /// Whether we should include rewritten candidates in the overload set. bool AllowRewrittenCandidates; @@ -1007,22 +1011,22 @@ CRK = OverloadCandidateRewriteKind(CRK | CRK_Reversed); return CRK; } - /// Determines whether this operator could be implemented by a function /// with reversed parameter order. bool isReversible() { return AllowRewrittenCandidates && OriginalOperator && (getRewrittenOverloadedOperator(OriginalOperator) != OO_None || - shouldAddReversed(OriginalOperator)); + mayAddReversed(OriginalOperator)); } - /// Determine whether we should consider looking for and adding reversed + /// Determine whether we may consider looking for and adding reversed /// candidates for operator Op. - bool shouldAddReversed(OverloadedOperatorKind Op); + bool mayAddReversed(OverloadedOperatorKind Op); /// Determine whether we should add a rewritten candidate for \p FD with /// reversed parameter order. - bool shouldAddReversed(ASTContext &Ctx, const FunctionDecl *FD); + bool shouldAddReversed(Sema &S, ArrayRef Args, + const FunctionDecl *FD); }; private: diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7867,7 +7867,8 @@ OverloadCandidateSet CandidateSet( FD->getLocation(), OverloadCandidateSet::CSK_Operator, OverloadCandidateSet::OperatorRewriteInfo( - OO, /*AllowRewrittenCandidates=*/!SpaceshipCandidates)); + OO, FD->getLocation(), + /*AllowRewrittenCandidates=*/!SpaceshipCandidates)); /// C++2a [class.compare.default]p1 [P2002R0]: /// [...] the defaulted function itself is never a candidate for overload diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp --- a/clang/lib/Sema/SemaOverload.cpp +++ b/clang/lib/Sema/SemaOverload.cpp @@ -12,14 +12,17 @@ #include "clang/AST/ASTContext.h" #include "clang/AST/CXXInheritance.h" +#include "clang/AST/DeclCXX.h" #include "clang/AST/DeclObjC.h" #include "clang/AST/DependenceFlags.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" #include "clang/AST/ExprObjC.h" +#include "clang/AST/Type.h" #include "clang/AST/TypeOrdering.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/DiagnosticOptions.h" +#include "clang/Basic/OperatorKinds.h" #include "clang/Basic/PartialDiagnostic.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/TargetInfo.h" @@ -34,6 +37,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallString.h" +#include "llvm/Support/Casting.h" #include #include @@ -890,7 +894,78 @@ } } -bool OverloadCandidateSet::OperatorRewriteInfo::shouldAddReversed( +static bool FunctionsCorrespond(ASTContext &Ctx, const FunctionDecl *X, + const FunctionDecl *Y) { + if (!X || !Y) + return false; + if (X->getNumParams() != Y->getNumParams()) + return false; + for (unsigned I = 0; I < X->getNumParams(); ++I) + if (!Ctx.hasSameUnqualifiedType(X->getParamDecl(I)->getType(), + Y->getParamDecl(I)->getType())) + return false; + if (auto *FTX = X->getDescribedFunctionTemplate()) { + auto *FTY = Y->getDescribedFunctionTemplate(); + if (!FTY) + return false; + if (!Ctx.isSameTemplateParameterList(FTX->getTemplateParameters(), + FTY->getTemplateParameters())) + return false; + } + return true; +} + +static bool shouldAddReversedEqEq(Sema &S, SourceLocation OpLoc, + Expr *FirstOperand, + const FunctionDecl *EqFD) { + assert(EqFD->getOverloadedOperator() == + OverloadedOperatorKind::OO_EqualEqual); + // C++2a [over.match.oper]p4: + // A non-template function or function template F named operator== is a + // rewrite target with first operand o unless a search for the name operator!= + // in the scope S from the instantiation context of the operator expression + // finds a function or function template that would correspond + // ([basic.scope.scope]) to F if its name were operator==, where S is the + // scope of the class type of o if F is a class member, and the namespace + // scope of which F is a member otherwise. A function template specialization + // named operator== is a rewrite target if its function template is a rewrite + // target. + DeclarationName NotEqOp = S.Context.DeclarationNames.getCXXOperatorName( + OverloadedOperatorKind::OO_ExclaimEqual); + if (auto *MD = dyn_cast(EqFD)) { + // If F is a class member, search scope is class type of first operand. + QualType RHS = FirstOperand->getType(); + auto *RHSRec = RHS->getAs(); + if (!RHSRec) + return true; + LookupResult Members(S, NotEqOp, OpLoc, + Sema::LookupNameKind::LookupMemberName); + S.LookupQualifiedName(Members, RHSRec->getDecl()); + Members.suppressDiagnostics(); + for (NamedDecl *Op : Members) + if (FunctionsCorrespond(S.Context, EqFD, Op->getAsFunction())) + return false; + return true; + } + // Otherwise the search scope is the namespace scope of which F is a member. + LookupResult NonMembers(S, NotEqOp, OpLoc, + Sema::LookupNameKind::LookupOperatorName); + S.LookupName(NonMembers, S.getScopeForContext(const_cast( + EqFD->getEnclosingNamespaceContext()))); + NonMembers.suppressDiagnostics(); + for (NamedDecl *Op : NonMembers) { + auto *FD = Op->getAsFunction(); + if(auto* UD = dyn_cast(Op)) + FD = UD->getUnderlyingDecl()->getAsFunction(); + if (FunctionsCorrespond(S.Context, EqFD, FD) && + declaresSameEntity(cast(EqFD->getDeclContext()), + cast(Op->getDeclContext()))) + return false; + } + return true; +} + +bool OverloadCandidateSet::OperatorRewriteInfo::mayAddReversed( OverloadedOperatorKind Op) { if (!AllowRewrittenCandidates) return false; @@ -898,14 +973,18 @@ } bool OverloadCandidateSet::OperatorRewriteInfo::shouldAddReversed( - ASTContext &Ctx, const FunctionDecl *FD) { - if (!shouldAddReversed(FD->getDeclName().getCXXOverloadedOperator())) + Sema &S, ArrayRef Args, const FunctionDecl *FD) { + auto Op = FD->getOverloadedOperator(); + if (!mayAddReversed(Op)) + return false; + if (Op == OverloadedOperatorKind::OO_EqualEqual && + !shouldAddReversedEqEq(S, OpLoc, Args[1], FD)) return false; // Don't bother adding a reversed candidate that can never be a better // match than the non-reversed version. return FD->getNumParams() != 2 || - !Ctx.hasSameUnqualifiedType(FD->getParamDecl(0)->getType(), - FD->getParamDecl(1)->getType()) || + !S.Context.hasSameUnqualifiedType(FD->getParamDecl(0)->getType(), + FD->getParamDecl(1)->getType()) || FD->hasAttr(); } @@ -7737,7 +7816,7 @@ if (FunTmpl) { AddTemplateOverloadCandidate(FunTmpl, F.getPair(), ExplicitTemplateArgs, FunctionArgs, CandidateSet); - if (CandidateSet.getRewriteInfo().shouldAddReversed(Context, FD)) + if (CandidateSet.getRewriteInfo().shouldAddReversed(*this, Args, FD)) AddTemplateOverloadCandidate( FunTmpl, F.getPair(), ExplicitTemplateArgs, {FunctionArgs[1], FunctionArgs[0]}, CandidateSet, false, false, @@ -7746,7 +7825,7 @@ if (ExplicitTemplateArgs) continue; AddOverloadCandidate(FD, F.getPair(), FunctionArgs, CandidateSet); - if (CandidateSet.getRewriteInfo().shouldAddReversed(Context, FD)) + if (CandidateSet.getRewriteInfo().shouldAddReversed(*this, Args, FD)) AddOverloadCandidate(FD, F.getPair(), {FunctionArgs[1], FunctionArgs[0]}, CandidateSet, false, false, true, false, ADLCallKind::NotADL, @@ -7797,12 +7876,17 @@ Operators.suppressDiagnostics(); for (LookupResult::iterator Oper = Operators.begin(), - OperEnd = Operators.end(); - Oper != OperEnd; - ++Oper) + OperEnd = Operators.end(); + Oper != OperEnd; ++Oper) { + if (Oper->getAsFunction() && + PO == OverloadCandidateParamOrder::Reversed && + !CandidateSet.getRewriteInfo().shouldAddReversed( + *this, {Args[1], Args[0]}, Oper->getAsFunction())) + continue; AddMethodCandidate(Oper.getPair(), Args[0]->getType(), Args[0]->Classify(Context), Args.slice(1), CandidateSet, /*SuppressUserConversion=*/false, PO); + } } } @@ -9498,7 +9582,7 @@ FD, FoundDecl, Args, CandidateSet, /*SuppressUserConversions=*/false, PartialOverloading, /*AllowExplicit=*/true, /*AllowExplicitConversion=*/false, ADLCallKind::UsesADL); - if (CandidateSet.getRewriteInfo().shouldAddReversed(Context, FD)) { + if (CandidateSet.getRewriteInfo().shouldAddReversed(*this, Args, FD)) { AddOverloadCandidate( FD, FoundDecl, {Args[1], Args[0]}, CandidateSet, /*SuppressUserConversions=*/false, PartialOverloading, @@ -9512,7 +9596,7 @@ /*SuppressUserConversions=*/false, PartialOverloading, /*AllowExplicit=*/true, ADLCallKind::UsesADL); if (CandidateSet.getRewriteInfo().shouldAddReversed( - Context, FTD->getTemplatedDecl())) { + *this, Args, FTD->getTemplatedDecl())) { AddTemplateOverloadCandidate( FTD, FoundDecl, ExplicitTemplateArgs, {Args[1], Args[0]}, CandidateSet, /*SuppressUserConversions=*/false, PartialOverloading, @@ -13625,14 +13709,14 @@ // Add operator candidates that are member functions. AddMemberOperatorCandidates(Op, OpLoc, Args, CandidateSet); - if (CandidateSet.getRewriteInfo().shouldAddReversed(Op)) + if (CandidateSet.getRewriteInfo().mayAddReversed(Op)) AddMemberOperatorCandidates(Op, OpLoc, {Args[1], Args[0]}, CandidateSet, OverloadCandidateParamOrder::Reversed); // In C++20, also add any rewritten member candidates. if (ExtraOp) { AddMemberOperatorCandidates(ExtraOp, OpLoc, Args, CandidateSet); - if (CandidateSet.getRewriteInfo().shouldAddReversed(ExtraOp)) + if (CandidateSet.getRewriteInfo().mayAddReversed(ExtraOp)) AddMemberOperatorCandidates(ExtraOp, OpLoc, {Args[1], Args[0]}, CandidateSet, OverloadCandidateParamOrder::Reversed); @@ -13763,9 +13847,9 @@ return CreateBuiltinBinOp(OpLoc, Opc, Args[0], Args[1]); // Build the overload set. - OverloadCandidateSet CandidateSet( - OpLoc, OverloadCandidateSet::CSK_Operator, - OverloadCandidateSet::OperatorRewriteInfo(Op, AllowRewrittenCandidates)); + OverloadCandidateSet CandidateSet(OpLoc, OverloadCandidateSet::CSK_Operator, + OverloadCandidateSet::OperatorRewriteInfo( + Op, OpLoc, AllowRewrittenCandidates)); if (DefaultedFn) CandidateSet.exclude(DefaultedFn); LookupOverloadedBinOp(CandidateSet, Op, Fns, Args, PerformADL); @@ -13840,6 +13924,22 @@ if (AmbiguousWithSelf) { Diag(FnDecl->getLocation(), diag::note_ovl_ambiguous_oper_binary_reversed_self); + // Mark member== const or provide matching != to disallow reversed + // args. Eg. + // struct S { bool operator==(const S&); }; + // S()==S(); + if (auto *MD = dyn_cast(FnDecl)) + if (Op == OverloadedOperatorKind::OO_EqualEqual && + !MD->isConst() && + Context.hasSameUnqualifiedType( + MD->getThisObjectType(), + MD->getParamDecl(0)->getType().getNonReferenceType()) && + Context.hasSameUnqualifiedType(MD->getThisObjectType(), + Args[0]->getType()) && + Context.hasSameUnqualifiedType(MD->getThisObjectType(), + Args[1]->getType())) + Diag(FnDecl->getLocation(), + diag::note_ovl_ambiguous_eqeq_reversed_self_non_const); } else { Diag(FnDecl->getLocation(), diag::note_ovl_ambiguous_oper_binary_selected_candidate); diff --git a/clang/test/CXX/over/over.match/over.match.funcs/over.match.oper/p3-2a.cpp b/clang/test/CXX/over/over.match/over.match.funcs/over.match.oper/p3-2a.cpp --- a/clang/test/CXX/over/over.match/over.match.funcs/over.match.oper/p3-2a.cpp +++ b/clang/test/CXX/over/over.match/over.match.funcs/over.match.oper/p3-2a.cpp @@ -125,46 +125,198 @@ bool b2 = 0 == ADL::type(); } -// Various C++17 cases that are known to be broken by the C++20 rules. -namespace problem_cases { - // We can have an ambiguity between an operator and its reversed form. This - // wasn't intended by the original "consistent comparison" proposal, and we - // allow it as extension, picking the non-reversed form. - struct A { - bool operator==(const A&); // expected-note {{ambiguity is between a regular call to this operator and a call with the argument order reversed}} - }; - bool cmp_non_const = A() == A(); // expected-warning {{ambiguous}} +namespace P2468R2 { +//=============Problem cases prior to P2468R2 but now intentionally rejected============= +struct SymmetricNonConst { + bool operator==(const SymmetricNonConst&); // expected-note {{ambiguity is between a regular call to this operator and a call with the argument order reversed}} + // expected-note@-1 {{mark operator== as const or add a matching operator!= to resolve the ambiguity}} +}; +bool cmp_non_const = SymmetricNonConst() == SymmetricNonConst(); // expected-warning {{ambiguous}} - struct B { - virtual bool operator==(const B&) const; - }; - struct D : B { - bool operator==(const B&) const override; // expected-note {{operator}} - }; - bool cmp_base_derived = D() == D(); // expected-warning {{ambiguous}} +struct SymmetricConst { + bool operator==(const SymmetricConst&) const; +}; +bool cmp_const = SymmetricConst() == SymmetricConst(); - template struct CRTPBase { - bool operator==(const T&) const; // expected-note {{operator}} expected-note {{reversed}} - bool operator!=(const T&) const; // expected-note {{non-reversed}} - }; - struct CRTP : CRTPBase {}; - bool cmp_crtp = CRTP() == CRTP(); // expected-warning-re {{ambiguous despite there being a unique best viable function{{$}}}}}} - bool cmp_crtp2 = CRTP() != CRTP(); // expected-warning {{ambiguous despite there being a unique best viable function with non-reversed arguments}} - - // Given a choice between a rewritten and non-rewritten function with the - // same parameter types, where the rewritten function is reversed and each - // has a better conversion for one of the two arguments, prefer the - // non-rewritten one. - using UBool = signed char; // ICU uses this. - struct ICUBase { - virtual UBool operator==(const ICUBase&) const; - UBool operator!=(const ICUBase &arg) const { return !operator==(arg); } - }; - struct ICUDerived : ICUBase { - UBool operator==(const ICUBase&) const override; // expected-note {{declared here}} expected-note {{ambiguity is between}} - }; - bool cmp_icu = ICUDerived() != ICUDerived(); // expected-warning {{ambiguous}} expected-warning {{'bool', not 'UBool'}} +struct B { + virtual bool operator==(const B&) const; +}; +struct D : B { + bool operator==(const B&) const override; // expected-note {{operator}} +}; +bool cmp_base_derived = D() == D(); // expected-warning {{ambiguous}} + +// Reversed "3" not used because we find "2". +// Rewrite != from "3" but warn that "chosen rewritten candidate must return cv-bool". +using UBool = signed char; +struct ICUBase { + virtual UBool operator==(const ICUBase&) const; // 1. + UBool operator!=(const ICUBase &arg) const { return !operator==(arg); } // 2. +}; +struct ICUDerived : ICUBase { + // 3. + UBool operator==(const ICUBase&) const override; // expected-note {{declared here}} +}; +bool cmp_icu = ICUDerived() != ICUDerived(); // expected-warning {{ISO C++20 requires return type of selected 'operator==' function for rewritten '!=' comparison to be 'bool', not 'UBool' (aka 'signed char')}} +// Accepted by P2468R2. +// 1 +struct S { + bool operator==(const S&) { return true; } + bool operator!=(const S&) { return false; } +}; +bool ts = S{} != S{}; +// 2 +template struct CRTPBase { + bool operator==(const T&) const; + bool operator!=(const T&) const; +}; +struct CRTP : CRTPBase {}; +bool cmp_crtp = CRTP() == CRTP(); +bool cmp_crtp2 = CRTP() != CRTP(); +// https://github.com/llvm/llvm-project/issues/57711 +namespace issue_57711 { +template +bool compare(T l, T r) + requires requires { l == r; } { + return l == r; +} + +void test() { + compare(CRTP(), CRTP()); // previously this was a hard error (due to SFINAE failure). +} +} +// 3 +template +struct GenericIterator { + using ConstIterator = GenericIterator; + using NonConstIterator = GenericIterator; + GenericIterator() = default; + GenericIterator(const NonConstIterator&); + + bool operator==(ConstIterator) const; + bool operator!=(ConstIterator) const; +}; +using Iterator = GenericIterator; + +bool biter = Iterator{} == Iterator{}; + +// Intentionally rejected by P2468R2 +struct ImplicitInt { + ImplicitInt(); + ImplicitInt(int*); + bool operator==(const ImplicitInt&) const; // expected-note {{reversed}} + operator int*() const; +}; +bool implicit_int = nullptr != ImplicitInt{}; // expected-error {{use of overloaded operator '!=' is ambiguous (with operand types 'std::nullptr_t' and 'ImplicitInt')}} + // expected-note@-1 4 {{built-in candidate operator!=}} + +// https://eel.is/c++draft/over.match.oper#example-2 +namespace example { +struct A {}; +template bool operator==(A, T); // 1. expected-note {{candidate function template not viable: no known conversion from 'int' to 'A' for 1st argument}} +bool a1 = 0 == A(); // OK, calls reversed 1 +template bool operator!=(A, T); +bool a2 = 0 == A(); // expected-error {{invalid operands to binary expression ('int' and 'A')}} + +struct B { + bool operator==(const B&); // 2 + // expected-note@-1 {{ambiguity is between a regular call to this operator and a call with the argument order reversed}} +}; +struct C : B { + C(); + C(B); + bool operator!=(const B&); // 3 +}; +bool c1 = B() == C(); // OK, calls 2; reversed 2 is not a candidate because search for operator!= in C finds #3 +bool c2 = C() == B(); // expected-warning {{ISO C++20 considers use of overloaded operator '==' (with operand types 'C' and 'B') to be ambiguous despite there being a unique best viable function}} + +struct D {}; +template bool operator==(D, T); // 4 +inline namespace N { + template bool operator!=(D, T); // 5 +} +bool d1 = 0 == D(); // OK, calls reversed 4; 5 does not forbid 4 as a rewrite target +} // namespace example + +namespace template_tests { +namespace template_head_does_not_match { +struct A {}; +template bool operator==(A, T); +template bool operator!=(A, T); +bool x = 0 == A(); // Ok. Use rewritten candidate. +} + +namespace template_with_different_param_name_are_equivalent { +struct A {}; +template bool operator==(A, T); // expected-note {{candidate function template not viable: no known conversion from 'int' to 'A' for 1st argument}} +template bool operator!=(A, U); +bool x = 0 == A(); // expected-error {{invalid operands to binary expression ('int' and 'A')}} +} + +namespace template_and_non_template { +struct A { +template bool operator==(const T&); +// expected-note@-1{{mark operator== as const or add a matching operator!= to resolve the ambiguity}} +// expected-note@-2{{ambiguity is between a regular call to this operator and a call with the argument order reversed}} +bool operator!=(const A&); // does not forbid rewrite. +}; +bool a = A() == A(); // expected-warning {{ ambiguous despite there being a unique best viable function}} + +struct B { +template bool operator==(const T&) const; +bool operator!=(const B&); +}; +bool b = B() == B(); // ok. No rewrite due to const. + +struct C {}; +template +bool operator==(C, int); +bool operator!=(C, int); +bool c = 0 == C(); // Ok. Use rewritten candidate. +} +} // template_tests + +namespace using_decls { +namespace simple { +struct C {}; +bool operator==(C, int); // expected-note {{candidate function not viable: no known conversion from 'int' to 'C' for 1st argument}} +bool a = 0 == C(); // Ok. Use rewritten candidate. +namespace other_ns { bool operator!=(C, int); } +bool b = 0 == C(); // Ok. Use rewritten candidate. +using other_ns::operator!=; +bool c = 0 == C(); // Rewrite not possible. expected-error {{invalid operands to binary expression ('int' and 'C')}} +} +namespace templated { +struct C {}; +template +bool operator==(C, T); // expected-note {{candidate function template not viable: no known conversion from 'int' to 'C' for 1st argument}} +bool a = 0 == C(); // Ok. Use rewritten candidate. +namespace other_ns { template bool operator!=(C, T); } +bool b = 0 == C(); // Ok. Use rewritten candidate. +using other_ns::operator!=; +bool c = 0 == C(); // Rewrite not possible. expected-error {{invalid operands to binary expression ('int' and 'C')}} +} // templated +} // using_decls +// FIXME: Match requires clause. +// namespace match_requires_clause { +// template +// struct A { +// bool operator==(const A&) requires (x==1); +// bool operator!=(const A&) requires (x==1); +// }; +// int a1 = A<1>() == A<1>(); +// } + +namespace static_operators { +// Verify no crash. +struct X { + bool operator ==(X const&); // expected-note {{ambiguity is between a regular call}} + // expected-note@-1 {{mark operator== as const or add a matching operator!= to resolve the ambiguity}} + static bool operator !=(X const&, X const&); // expected-error {{overloaded 'operator!=' cannot be a static member function}} +}; +bool x = X() == X(); // expected-warning {{ambiguous}} } +} // namespace P2468R2 #else // NO_ERRORS