From cafc7416baf7eecef8ecaf05802f2f7c0da725c0 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 4 Dec 2019 15:25:27 -0800 Subject: [PATCH] [c++20] Synthesis of defaulted comparison functions. Array members are not yet handled. In addition, defaulted comparisons can't yet find comparison operators by unqualified lookup (only by member lookup and ADL). These issues will be fixed in follow-on changes. --- clang/include/clang/AST/Decl.h | 10 + clang/include/clang/AST/DeclCXX.h | 10 - clang/include/clang/Basic/DiagnosticSemaKinds.td | 5 +- clang/include/clang/Sema/Sema.h | 9 +- clang/lib/AST/ExprConstant.cpp | 30 ++ clang/lib/Sema/SemaDeclCXX.cpp | 341 +++++++++++++++++++-- clang/lib/Sema/SemaExpr.cpp | 9 +- clang/lib/Sema/SemaOverload.cpp | 126 +++++++- clang/lib/Sema/SemaTemplateInstantiate.cpp | 20 +- .../class.compare/class.compare.default/p2.cpp | 2 +- .../class.compare/class.compare.default/p5.cpp | 45 +++ clang/test/CXX/class/class.compare/class.eq/p2.cpp | 18 +- clang/test/CXX/class/class.compare/class.eq/p3.cpp | 11 + .../test/CXX/class/class.compare/class.rel/p2.cpp | 23 +- .../CXX/class/class.compare/class.spaceship/p1.cpp | 108 ++++++- .../CXX/class/class.compare/class.spaceship/p3.cpp | 35 +++ 16 files changed, 743 insertions(+), 59 deletions(-) create mode 100644 clang/test/CXX/class/class.compare/class.compare.default/p5.cpp create mode 100644 clang/test/CXX/class/class.compare/class.eq/p3.cpp create mode 100644 clang/test/CXX/class/class.compare/class.spaceship/p3.cpp diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h index f491354..2d1a306 100644 --- a/clang/include/clang/AST/Decl.h +++ b/clang/include/clang/AST/Decl.h @@ -2115,6 +2115,16 @@ public: FunctionDeclBits.IsExplicitlyDefaulted = ED; } + /// True if this method is user-declared and was not + /// deleted or defaulted on its first declaration. + bool isUserProvided() const { + auto *DeclAsWritten = this; + if (FunctionDecl *Pattern = getTemplateInstantiationPattern()) + DeclAsWritten = Pattern; + return !(DeclAsWritten->isDeleted() || + DeclAsWritten->getCanonicalDecl()->isDefaulted()); + } + /// Whether falling off this function implicitly returns null/zero. /// If a more specific implicit return value is required, front-ends /// should synthesize the appropriate return statements. diff --git a/clang/include/clang/AST/DeclCXX.h b/clang/include/clang/AST/DeclCXX.h index 0f2018f..0043ce1 100644 --- a/clang/include/clang/AST/DeclCXX.h +++ b/clang/include/clang/AST/DeclCXX.h @@ -1995,16 +1995,6 @@ public: return const_cast(this)->getMostRecentDecl(); } - /// True if this method is user-declared and was not - /// deleted or defaulted on its first declaration. - bool isUserProvided() const { - auto *DeclAsWritten = this; - if (auto *Pattern = getTemplateInstantiationPattern()) - DeclAsWritten = cast(Pattern); - return !(DeclAsWritten->isDeleted() || - DeclAsWritten->getCanonicalDecl()->isDefaulted()); - } - void addOverriddenMethod(const CXXMethodDecl *MD); using method_iterator = const CXXMethodDecl *const *; diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index dcfc8fb..9392870 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -1732,7 +1732,10 @@ def note_ivar_decl : Note<"instance variable is declared here">; def note_bitfield_decl : Note<"bit-field is declared here">; def note_implicit_param_decl : Note<"%0 is an implicit parameter">; def note_member_synthesized_at : Note< - "in implicit %sub{select_special_member_kind}0 for %1 " + "in %select{implicit|defaulted}0 %sub{select_special_member_kind}1 for %2 " + "first required here">; +def note_comparison_synthesized_at : Note< + "in defaulted %sub{select_defaulted_comparison_kind}0 for %1 " "first required here">; def err_missing_default_ctor : Error< "%select{constructor for %1 must explicitly initialize the|" diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index ed1f137..2b0db07 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -3319,7 +3319,12 @@ public: const UnresolvedSetImpl &Fns, Expr *LHS, Expr *RHS, bool RequiresADL = true, - bool AllowRewrittenCandidates = true); + bool AllowRewrittenCandidates = true, + FunctionDecl *DefaultedFn = nullptr); + ExprResult BuildSynthesizedThreeWayComparison(SourceLocation OpLoc, + const UnresolvedSetImpl &Fns, + Expr *LHS, Expr *RHS, + FunctionDecl *DefaultedFn); ExprResult CreateOverloadedArraySubscriptExpr(SourceLocation LLoc, SourceLocation RLoc, @@ -6516,6 +6521,8 @@ public: bool CheckExplicitlyDefaultedComparison(FunctionDecl *MD, DefaultedComparisonKind DCK); + void DefineDefaultedComparison(SourceLocation Loc, FunctionDecl *FD, + DefaultedComparisonKind DCK); //===--------------------------------------------------------------------===// // C++ Derived Classes diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp index 3151ec0..5aa1519 100644 --- a/clang/lib/AST/ExprConstant.cpp +++ b/clang/lib/AST/ExprConstant.cpp @@ -6830,6 +6830,36 @@ public: return StmtVisitorTy::Visit(Source); } + bool VisitPseudoObjectExpr(const PseudoObjectExpr *E) { + for (const Expr *SemE : E->semantics()) { + if (auto *OVE = dyn_cast(SemE)) { + // FIXME: We can't handle the case where an OpaqueValueExpr is also the + // result expression: there could be two different LValues that would + // refer to the same object in that case, and we can't model that. + if (SemE == E->getResultExpr()) + return Error(E); + + // Unique OVEs get evaluated if and when we encounter them when + // emitting the rest of the semantic form, rather than eagerly. + if (OVE->isUnique()) + continue; + + LValue LV; + if (!Evaluate(Info.CurrentCall->createTemporary( + OVE, getStorageType(Info.Ctx, OVE), false, LV), + Info, OVE->getSourceExpr())) + return false; + } else if (SemE == E->getResultExpr()) { + if (!StmtVisitorTy::Visit(SemE)) + return false; + } else { + if (!EvaluateIgnoredValue(Info, SemE)) + return false; + } + } + return true; + } + bool VisitCallExpr(const CallExpr *E) { APValue Result; if (!handleCallExpr(E, Result, nullptr)) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index ba516b6..c8b9598 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7085,7 +7085,8 @@ namespace { /// /// This is accomplished by performing two visitation steps over the eventual /// body of the function. -template +template class DefaultedComparisonVisitor { public: using DefaultedComparisonKind = Sema::DefaultedComparisonKind; @@ -7094,45 +7095,54 @@ public: DefaultedComparisonKind DCK) : S(S), RD(RD), FD(FD), DCK(DCK) {} - Result visit() { + ResultList visit() { // The type of an lvalue naming a parameter of this function. QualType ParamLvalType = FD->getParamDecl(0)->getType().getNonReferenceType(); + ResultList Results; + switch (DCK) { case DefaultedComparisonKind::None: llvm_unreachable("not a defaulted comparison"); case DefaultedComparisonKind::Equal: case DefaultedComparisonKind::ThreeWay: - return getDerived().visitSubobjects(RD, ParamLvalType.getQualifiers()); + getDerived().visitSubobjects(Results, RD, ParamLvalType.getQualifiers()); + return Results; case DefaultedComparisonKind::NotEqual: case DefaultedComparisonKind::Relational: - return getDerived().visitExpandedSubobject( - ParamLvalType, getDerived().getCompleteObject()); + Results.add(getDerived().visitExpandedSubobject( + ParamLvalType, getDerived().getCompleteObject())); + return Results; } } protected: Derived &getDerived() { return static_cast(*this); } - Result visitSubobjects(CXXRecordDecl *Record, Qualifiers Quals) { - Result R; - // C++ [class.compare.default]p5: - // The direct base class subobjects of C [...] + /// Visit the expanded list of subobjects of the given type, as specified in + /// C++2a [class.compare.default]. + /// + /// \return \c true if the ResultList object said we're done, \c false if not. + bool visitSubobjects(ResultList &Results, CXXRecordDecl *Record, + Qualifiers Quals) { + // C++2a [class.compare.default]p4: + // The direct base class subobjects of C for (CXXBaseSpecifier &Base : Record->bases()) - if (R.add(getDerived().visitSubobject( + if (Results.add(getDerived().visitSubobject( S.Context.getQualifiedType(Base.getType(), Quals), getDerived().getBase(&Base)))) - return R; - // followed by the non-static data members of C [...] + return true; + + // followed by the non-static data members of C for (FieldDecl *Field : Record->fields()) { // Recursively expand anonymous structs. if (Field->isAnonymousStructOrUnion()) { - if (R.add( - visitSubobjects(Field->getType()->getAsCXXRecordDecl(), Quals))) - return R; + if (visitSubobjects(Results, Field->getType()->getAsCXXRecordDecl(), + Quals)) + return true; continue; } @@ -7143,12 +7153,13 @@ protected: QualType FieldType = S.Context.getQualifiedType(Field->getType(), FieldQuals); - if (R.add(getDerived().visitSubobject(FieldType, - getDerived().getField(Field)))) - return R; + if (Results.add(getDerived().visitSubobject( + FieldType, getDerived().getField(Field)))) + return true; } + // form a list of subobjects. - return R; + return false; } Result visitSubobject(QualType Type, Subobject Subobj) { @@ -7200,6 +7211,7 @@ struct DefaultedComparisonSubobject { class DefaultedComparisonAnalyzer : public DefaultedComparisonVisitor { public: enum DiagnosticKind { NoDiagnostics, ExplainDeleted, ExplainConstexpr }; @@ -7407,6 +7419,260 @@ private: return R; } }; + +/// A list of statements. +struct StmtListResult { + bool IsInvalid = false; + llvm::SmallVector Stmts; + + bool add(const StmtResult &S) { + IsInvalid |= S.isInvalid(); + if (IsInvalid) + return true; + Stmts.push_back(S.get()); + return false; + } +}; + +/// A visitor over the notional body of a defaulted comparison that synthesizes +/// the actual body. +class DefaultedComparisonSynthesizer + : public DefaultedComparisonVisitor> { + SourceLocation Loc; + +public: + using Base = DefaultedComparisonVisitor; + using ExprPair = std::pair; + + friend Base; + + DefaultedComparisonSynthesizer(Sema &S, CXXRecordDecl *RD, FunctionDecl *FD, + DefaultedComparisonKind DCK, + SourceLocation BodyLoc) + : Base(S, RD, FD, DCK), Loc(BodyLoc) {} + + /// Build a suitable function body for this defaulted comparison operator. + StmtResult build() { + Sema::CompoundScopeRAII CompoundScope(S); + + StmtListResult Stmts = visit(); + if (Stmts.IsInvalid) + return StmtError(); + + ExprResult RetVal; + switch (DCK) { + case DefaultedComparisonKind::None: + llvm_unreachable("not a defaulted comparison"); + + case DefaultedComparisonKind::Equal: + // C++2a [class.eq]p3: + // [...] compar[e] the corresponding elements [...] until the first + // index i where xi == yi yields [...] false. If no such index exists, + // V is true. Otherwise, V is false. + // + // Join the comparisons with '&&'s and return the result. Use a right + // fold because that short-circuits more naturally. + for (Stmt *EAsStmt : llvm::reverse(Stmts.Stmts)) { + Expr *E = cast(EAsStmt); + if (RetVal.isUnset()) { + RetVal = E; + continue; + } + RetVal = S.CreateBuiltinBinOp(Loc, BO_LAnd, E, RetVal.get()); + if (RetVal.isInvalid()) + return StmtError(); + } + // If no such index exists, V is true. + if (RetVal.isUnset()) + RetVal = S.ActOnCXXBoolLiteral(Loc, tok::kw_true); + Stmts.Stmts.clear(); + break; + + case DefaultedComparisonKind::ThreeWay: { + // Per C++2a [class.spaceship]p3, as a fallback add: + // return static_cast(std::strong_ordering::equal); + QualType StrongOrdering = S.CheckComparisonCategoryType( + ComparisonCategoryType::StrongOrdering, Loc); + if (StrongOrdering.isNull()) + return StmtError(); + VarDecl *EqualVD = S.Context.CompCategories.getInfoForType(StrongOrdering) + .getValueInfo(ComparisonCategoryResult::Equal) + ->VD; + RetVal = S.BuildDeclarationNameExpr( + CXXScopeSpec(), DeclarationNameInfo(), EqualVD); + if (RetVal.isInvalid()) + return StmtError(); + RetVal = buildStaticCastToR(RetVal.get()); + break; + } + + case DefaultedComparisonKind::NotEqual: + case DefaultedComparisonKind::Relational: + RetVal = cast(Stmts.Stmts.pop_back_val()); + break; + } + + // Build the final return statement. + if (RetVal.isInvalid()) + return StmtError(); + StmtResult ReturnStmt = S.BuildReturnStmt(Loc, RetVal.get()); + if (ReturnStmt.isInvalid()) + return StmtError(); + Stmts.Stmts.push_back(ReturnStmt.get()); + + return S.ActOnCompoundStmt(Loc, Loc, Stmts.Stmts, /*IsStmtExpr=*/false); + } + +private: + ExprResult getParam(unsigned I) { + ParmVarDecl *PD = FD->getParamDecl(I); + return S.BuildDeclarationNameExpr( + CXXScopeSpec(), DeclarationNameInfo(PD->getDeclName(), Loc), PD); + } + + ExprPair getCompleteObject() { + unsigned Param = 0; + ExprResult LHS; + if (isa(FD)) { + // LHS is '*this'. + LHS = S.ActOnCXXThis(Loc); + if (!LHS.isInvalid()) + LHS = S.CreateBuiltinUnaryOp(Loc, UO_Deref, LHS.get()); + } else { + LHS = getParam(Param++); + } + ExprResult RHS = getParam(Param++); + assert(Param == FD->getNumParams()); + return {LHS, RHS}; + } + + ExprPair getBase(CXXBaseSpecifier *Base) { + ExprPair Obj = getCompleteObject(); + if (Obj.first.isInvalid() || Obj.second.isInvalid()) + return {ExprError(), ExprError()}; + CXXCastPath Path = {Base}; + return {S.ImpCastExprToType(Obj.first.get(), Base->getType(), + CK_DerivedToBase, VK_LValue, &Path), + S.ImpCastExprToType(Obj.second.get(), Base->getType(), + CK_DerivedToBase, VK_LValue, &Path)}; + } + + ExprPair getField(FieldDecl *Field) { + ExprPair Obj = getCompleteObject(); + if (Obj.first.isInvalid() || Obj.second.isInvalid()) + return {ExprError(), ExprError()}; + + DeclAccessPair Found = DeclAccessPair::make(Field, Field->getAccess()); + DeclarationNameInfo NameInfo(Field->getDeclName(), Loc); + return {S.BuildFieldReferenceExpr(Obj.first.get(), /*IsArrow=*/false, Loc, + CXXScopeSpec(), Field, Found, NameInfo), + S.BuildFieldReferenceExpr(Obj.second.get(), /*IsArrow=*/false, Loc, + CXXScopeSpec(), Field, Found, NameInfo)}; + } + + // FIXME: When expanding a subobject, register a note in the code synthesis + // stack to say which subobject we're comparing. + + // FIXME: Build a loop for an array subobject. + + StmtResult visitExpandedSubobject(QualType Type, ExprPair Obj) { + UnresolvedSet<4> Fns; // FIXME: Track this. + + if (Obj.first.isInvalid() || Obj.second.isInvalid()) + return StmtError(); + + OverloadedOperatorKind OO = FD->getOverloadedOperator(); + ExprResult Op = S.CreateOverloadedBinOp( + Loc, BinaryOperator::getOverloadedOpcode(OO), Fns, + Obj.first.get(), Obj.second.get(), /*PerformADL=*/true, + /*AllowRewrittenCandidates=*/true, FD); + if (Op.isInvalid()) + return StmtError(); + + switch (DCK) { + case DefaultedComparisonKind::None: + llvm_unreachable("not a defaulted comparison"); + + case DefaultedComparisonKind::Equal: + // Per C++2a [class.eq]p2, each comparison is individually contextually + // converted to bool. + Op = S.PerformContextuallyConvertToBool(Op.get()); + if (Op.isInvalid()) + return StmtError(); + return Op.get(); + + case DefaultedComparisonKind::ThreeWay: { + // Per C++2a [class.spaceship]p3, form: + // if (R cmp = static_cast(op); cmp != 0) + // return cmp; + QualType R = FD->getReturnType(); + Op = buildStaticCastToR(Op.get()); + if (Op.isInvalid()) + return StmtError(); + + // R cmp = ...; + IdentifierInfo *Name = &S.Context.Idents.get("cmp"); + VarDecl *VD = + VarDecl::Create(S.Context, S.CurContext, Loc, Loc, Name, R, + S.Context.getTrivialTypeSourceInfo(R, Loc), SC_None); + S.AddInitializerToDecl(VD, Op.get(), /*DirectInit=*/false); + Stmt *InitStmt = new (S.Context) DeclStmt(DeclGroupRef(VD), Loc, Loc); + + // cmp != 0 + ExprResult VDRef = S.BuildDeclarationNameExpr( + CXXScopeSpec(), DeclarationNameInfo(Name, Loc), VD); + if (VDRef.isInvalid()) + return StmtError(); + llvm::APInt ZeroVal(S.Context.getIntWidth(S.Context.IntTy), 0); + Expr *Zero = + IntegerLiteral::Create(S.Context, ZeroVal, S.Context.IntTy, Loc); + ExprResult Comp = S.CreateOverloadedBinOp(Loc, BO_NE, Fns, VDRef.get(), + Zero, true, true, FD); + if (Comp.isInvalid()) + return StmtError(); + Sema::ConditionResult Cond = S.ActOnCondition( + nullptr, Loc, Comp.get(), Sema::ConditionKind::Boolean); + if (Cond.isInvalid()) + return StmtError(); + + // return cmp; + VDRef = S.BuildDeclarationNameExpr( + CXXScopeSpec(), DeclarationNameInfo(Name, Loc), VD); + if (VDRef.isInvalid()) + return StmtError(); + StmtResult ReturnStmt = S.BuildReturnStmt(Loc, VDRef.get()); + if (ReturnStmt.isInvalid()) + return StmtError(); + + // if (...) + return S.ActOnIfStmt(Loc, /*IsConstexpr=*/false, InitStmt, Cond, + ReturnStmt.get(), /*ElseLoc=*/SourceLocation(), + /*Else=*/nullptr); + } + + case DefaultedComparisonKind::NotEqual: + case DefaultedComparisonKind::Relational: + // C++2a [class.compare.secondary]p2: + // Otherwise, the operator function yields x @ y. + return Op.get(); + } + } + + /// Build "static_cast(E)". + ExprResult buildStaticCastToR(Expr *E) { + QualType R = FD->getReturnType(); + assert(!R->isUndeducedType() && "type should have been deduced already"); + + // Don't bother forming a no-op cast in the common case. + if (E->isRValue() && S.Context.hasSameType(E->getType(), R)) + return E; + return S.BuildCXXNamedCast(Loc, tok::kw_static_cast, + S.Context.getTrivialTypeSourceInfo(R, Loc), E, + SourceRange(Loc, Loc), SourceRange(Loc, Loc)); + } +}; } bool Sema::CheckExplicitlyDefaultedComparison(FunctionDecl *FD, @@ -7540,6 +7806,43 @@ bool Sema::CheckExplicitlyDefaultedComparison(FunctionDecl *FD, return false; } +void Sema::DefineDefaultedComparison(SourceLocation UseLoc, FunctionDecl *FD, + DefaultedComparisonKind DCK) { + assert(FD->isDefaulted() && !FD->isDeleted() && + !FD->doesThisDeclarationHaveABody()); + if (FD->willHaveBody() || FD->isInvalidDecl()) + return; + + SynthesizedFunctionScope Scope(*this, FD); + + // The exception specification is needed because we are defining the + // function. + // FIXME: Handle this better. Computing the exception specification will + // eventually need the function body. + ResolveExceptionSpec(UseLoc, FD->getType()->castAs()); + + // Add a context note for diagnostics produced after this point. + Scope.addContextNote(UseLoc); + + // Build and set up the function body. + { + CXXRecordDecl *RD = cast(FD->getLexicalParent()); + SourceLocation BodyLoc = + FD->getEndLoc().isValid() ? FD->getEndLoc() : FD->getLocation(); + StmtResult Body = + DefaultedComparisonSynthesizer(*this, RD, FD, DCK, BodyLoc).build(); + if (Body.isInvalid()) { + FD->setInvalidDecl(); + return; + } + FD->setBody(Body.get()); + FD->markUsed(Context); + } + + if (ASTMutationListener *L = getASTMutationListener()) + L->CompletedImplicitDefinition(FD); +} + void Sema::CheckDelayedMemberExceptionSpecs() { decltype(DelayedOverridingExceptionSpecChecks) Overriding; decltype(DelayedEquivalentExceptionSpecChecks) Equivalent; diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 5eeeba3..b97352e 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -15416,9 +15416,8 @@ static OdrUseContext isOdrUseContext(Sema &SemaRef) { } static bool isImplicitlyDefinableConstexprFunction(FunctionDecl *Func) { - CXXMethodDecl *MD = dyn_cast(Func); return Func->isConstexpr() && - (Func->isImplicitlyInstantiable() || (MD && !MD->isUserProvided())); + (Func->isImplicitlyInstantiable() || !Func->isUserProvided()); } /// Mark a function referenced, and check whether it is odr-used @@ -15566,6 +15565,12 @@ void Sema::MarkFunctionReferenced(SourceLocation Loc, FunctionDecl *Func, MarkVTableUsed(Loc, MethodDecl->getParent()); } + if (Func->isDefaulted() && !Func->isDeleted()) { + DefaultedComparisonKind DCK = getDefaultedComparisonKind(Func); + if (DCK != DefaultedComparisonKind::None) + DefineDefaultedComparison(Loc, Func, DCK); + } + // Implicit instantiation of function templates and member functions of // class templates. if (Func->isImplicitlyInstantiable()) { diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp index 27e1101..344e54b 100644 --- a/clang/lib/Sema/SemaOverload.cpp +++ b/clang/lib/Sema/SemaOverload.cpp @@ -12835,11 +12835,19 @@ void Sema::LookupOverloadedBinOp(OverloadCandidateSet &CandidateSet, /// /// \param LHS Left-hand argument. /// \param RHS Right-hand argument. +/// \param PerformADL Whether to consider operator candidates found by ADL. +/// \param AllowRewrittenCandidates Whether to consider candidates found by +/// C++20 operator rewrites. +/// \param DefaultedFn If we are synthesizing a defaulted operator function, +/// the function in question. Such a function is never a candidate in +/// our overload resolution. This also enables synthesizing a three-way +/// comparison from < and == as described in C++20 [class.spaceship]p1. ExprResult Sema::CreateOverloadedBinOp(SourceLocation OpLoc, BinaryOperatorKind Opc, const UnresolvedSetImpl &Fns, Expr *LHS, Expr *RHS, bool PerformADL, - bool AllowRewrittenCandidates) { + bool AllowRewrittenCandidates, + FunctionDecl *DefaultedFn) { Expr *Args[2] = { LHS, RHS }; LHS=RHS=nullptr; // Please use only Args instead of LHS/RHS couple @@ -12906,6 +12914,8 @@ ExprResult Sema::CreateOverloadedBinOp(SourceLocation OpLoc, OverloadCandidateSet CandidateSet( OpLoc, OverloadCandidateSet::CSK_Operator, OverloadCandidateSet::OperatorRewriteInfo(Op, AllowRewrittenCandidates)); + if (DefaultedFn) + CandidateSet.exclude(DefaultedFn); LookupOverloadedBinOp(CandidateSet, Op, Fns, Args, PerformADL); bool HadMultipleCandidates = (CandidateSet.size() > 1); @@ -13113,6 +13123,15 @@ ExprResult Sema::CreateOverloadedBinOp(SourceLocation OpLoc, if (Opc == BO_Comma) break; + // When defaulting an 'operator<=>', we can try to synthesize a three-way + // compare result using '==' and '<'. + if (DefaultedFn && Opc == BO_Cmp) { + ExprResult E = BuildSynthesizedThreeWayComparison(OpLoc, Fns, Args[0], + Args[1], DefaultedFn); + if (E.isInvalid() || E.isUsable()) + return E; + } + // For class as left operand for assignment or compound assignment // operator do not fall through to handling in built-in, but report that // no overloaded assignment operator found @@ -13194,6 +13213,111 @@ ExprResult Sema::CreateOverloadedBinOp(SourceLocation OpLoc, return CreateBuiltinBinOp(OpLoc, Opc, Args[0], Args[1]); } +ExprResult Sema::BuildSynthesizedThreeWayComparison( + SourceLocation OpLoc, const UnresolvedSetImpl &Fns, Expr *LHS, Expr *RHS, + FunctionDecl *DefaultedFn) { + const ComparisonCategoryInfo *Info = + Context.CompCategories.lookupInfoForType(DefaultedFn->getReturnType()); + // If we're not producing a known comparison category type, we can't + // synthesize a three-way comparison. Let the caller diagnose this. + if (!Info) + return ExprResult((Expr*)nullptr); + + // If we ever want to perform this synthesis more generally, we will need to + // apply the temporary materialization conversion to the operands. + assert(LHS->isGLValue() && RHS->isGLValue() && + "cannot use prvalue expressions more than once"); + Expr *OrigLHS = LHS; + Expr *OrigRHS = RHS; + + // Replace the LHS and RHS with OpaqueValueExprs; we're going to refer to + // each of them multiple times below. + LHS = new (Context) + OpaqueValueExpr(LHS->getExprLoc(), LHS->getType(), LHS->getValueKind(), + LHS->getObjectKind(), LHS); + RHS = new (Context) + OpaqueValueExpr(RHS->getExprLoc(), RHS->getType(), RHS->getValueKind(), + RHS->getObjectKind(), RHS); + + ExprResult Eq = CreateOverloadedBinOp(OpLoc, BO_EQ, Fns, LHS, RHS, true, true, + DefaultedFn); + if (Eq.isInvalid()) + return ExprError(); + + ExprResult Less; + if (Info->isOrdered()) { + Less = CreateOverloadedBinOp(OpLoc, BO_LT, Fns, LHS, RHS, true, true, + DefaultedFn); + if (Less.isInvalid()) + return ExprError(); + } + + ExprResult Greater; + if (Info->isOrdered()) { + Greater = CreateOverloadedBinOp(OpLoc, BO_LT, Fns, RHS, LHS, true, true, + DefaultedFn); + if (Greater.isInvalid()) + return ExprError(); + } + + // Form the list of comparisons we're going to perform. + struct Comparison { + ExprResult Cmp; + ComparisonCategoryResult Result; + } Comparisons[4] = + { {Eq, Info->isStrong() ? ComparisonCategoryResult::Equal + : ComparisonCategoryResult::Equivalent}, + {Less, ComparisonCategoryResult::Less}, + {Greater, ComparisonCategoryResult::Greater}, + {ExprResult(), ComparisonCategoryResult::Unordered}, + }; + + int I; + if (Info->isEquality()) { + Comparisons[1].Result = Info->isStrong() + ? ComparisonCategoryResult::Nonequal + : ComparisonCategoryResult::Nonequivalent; + I = 1; + } else if (!Info->isPartial()) { + I = 2; + } else { + I = 3; + } + + // Combine the comparisons with suitable conditional expressions. + ExprResult Result; + for (; I >= 0; --I) { + // Build a reference to the comparison category constant. + auto *VI = Info->lookupValueInfo(Comparisons[I].Result); + // FIXME: Missing a constant for a comparison category. Diagnose this? + if (!VI) + return ExprResult((Expr*)nullptr); + ExprResult ThisResult = + BuildDeclarationNameExpr(CXXScopeSpec(), DeclarationNameInfo(), VI->VD); + if (ThisResult.isInvalid()) + return ExprError(); + + // Build a conditional unless this is the final case. + if (Result.get()) { + Result = ActOnConditionalOp(OpLoc, OpLoc, Comparisons[I].Cmp.get(), + ThisResult.get(), Result.get()); + if (Result.isInvalid()) + return ExprError(); + } else { + Result = ThisResult; + } + } + + // Build a PseudoObjectExpr to model the rewriting of an <=> operator, and to + // bind the OpaqueValueExprs before they're (repeatedly) used. + Expr *SyntacticForm = new (Context) + BinaryOperator(OrigLHS, OrigRHS, BO_Cmp, Result.get()->getType(), + Result.get()->getValueKind(), + Result.get()->getObjectKind(), OpLoc, FPFeatures); + Expr *SemanticForm[] = {LHS, RHS, Result.get()}; + return PseudoObjectExpr::Create(Context, SyntacticForm, SemanticForm, 2); +} + ExprResult Sema::CreateOverloadedArraySubscriptExpr(SourceLocation LLoc, SourceLocation RLoc, diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index 2496c91..4d54ec1 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -672,13 +672,23 @@ void Sema::PrintInstantiationStack() { break; case CodeSynthesisContext::DefiningSynthesizedFunction: { - // FIXME: For synthesized members other than special members, produce a note. - auto *MD = dyn_cast(Active->Entity); - auto CSM = MD ? getSpecialMember(MD) : CXXInvalid; - if (CSM != CXXInvalid) { + // FIXME: For synthesized functions that are not defaulted, + // produce a note. + auto *FD = dyn_cast(Active->Entity); + DefaultedFunctionKind DFK = + FD ? getDefaultedFunctionKind(FD) : DefaultedFunctionKind(); + if (DFK.isSpecialMember()) { + auto *MD = cast(FD); Diags.Report(Active->PointOfInstantiation, diag::note_member_synthesized_at) - << CSM << Context.getTagDeclType(MD->getParent()); + << MD->isExplicitlyDefaulted() << DFK.asSpecialMember() + << Context.getTagDeclType(MD->getParent()); + } else if (DFK.isComparison()) { + Diags.Report(Active->PointOfInstantiation, + diag::note_comparison_synthesized_at) + << (int)DFK.asComparison() + << Context.getTagDeclType( + cast(FD->getLexicalDeclContext())); } break; } diff --git a/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp b/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp index bbc9060..cdffd44 100644 --- a/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp +++ b/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp @@ -14,7 +14,7 @@ struct A2 { bool operator==(const A2&) const; bool operator!=(const A2&) const = default; - bool operator<=>(const A2&) const; + int operator<=>(const A2&) const; bool operator<(const A2&) const = default; bool operator<=(const A2&) const = default; bool operator>(const A2&) const = default; diff --git a/clang/test/CXX/class/class.compare/class.compare.default/p5.cpp b/clang/test/CXX/class/class.compare/class.compare.default/p5.cpp new file mode 100644 index 0000000..f863ed0 --- /dev/null +++ b/clang/test/CXX/class/class.compare/class.compare.default/p5.cpp @@ -0,0 +1,45 @@ +// RUN: %clang_cc1 -std=c++2a -verify %s + +// expected-no-diagnostics +namespace std { + struct strong_ordering { + int n; + constexpr operator int() const { return n; } + static const strong_ordering less, equal, greater; + }; + constexpr strong_ordering strong_ordering::less{-1}, strong_ordering::equal{0}, strong_ordering::greater{1}; +} + +// Check that we compare subobjects in the right order. +struct Log { + char buff[8] = {}; + int n = 0; + constexpr void add(char c) { buff[n++] = c; } + constexpr bool operator==(const char *p) const { return __builtin_strcmp(p, buff) == 0; } +}; + +template struct B { + Log *log; + constexpr bool operator==(const B&) const { log->add(C); return true; } + constexpr std::strong_ordering operator<=>(const B&) const { log->add(C); return {0}; } +}; + +struct C : B<'a'>, B<'b'> { + B<'c'> c; + B<'d'> d; + // FIXME: Test arrays once we handle them properly. + + constexpr C(Log *p) : B<'a'>{p}, B<'b'>{p}, c{p}, d{p} {} + + bool operator==(const C&) const = default; + std::strong_ordering operator<=>(const C&) const = default; +}; + +constexpr bool check(bool which) { + Log log; + C c(&log); + (void)(which ? c == c : c <=> c); + return log == "abcd"; +} +static_assert(check(false)); +static_assert(check(true)); diff --git a/clang/test/CXX/class/class.compare/class.eq/p2.cpp b/clang/test/CXX/class/class.compare/class.eq/p2.cpp index 1a515de..d53d071 100644 --- a/clang/test/CXX/class/class.compare/class.eq/p2.cpp +++ b/clang/test/CXX/class/class.compare/class.eq/p2.cpp @@ -8,13 +8,16 @@ struct D { // expected-note@+1 {{candidate function (with reversed parameter order) not viable: 1st argument ('const}} bool operator==(D); }; -struct E { E(const E&) = delete; int operator==(E) const; }; +struct E { + E(const E &) = delete; // expected-note {{deleted}} + int operator==(E) const; // expected-note {{passing}} +}; struct F { void operator==(F) const; }; struct G { bool operator==(G) const = delete; }; // expected-note {{deleted here}} template struct X { X(); - bool operator==(const X&) const = default; // expected-note 3{{deleted here}} + bool operator==(const X&) const = default; // #x expected-note 3{{deleted here}} T t; // expected-note 2{{because there is no viable comparison function for member 't'}} // expected-note@-1 {{because it would invoke a deleted comparison function for member 't'}} }; @@ -31,10 +34,13 @@ void test() { void(X() == X()); // expected-error {{cannot be compared because its 'operator==' is implicitly deleted}} void(Mutable() == Mutable()); - // FIXME: Not deleted, but once we start synthesizing comparison function definitions, we should reject this. - void(X() == X()); - // FIXME: Similarly, not deleted under P2002R0, but synthesized body is ill-formed. - void(X() == X()); + // FIXME: We would benefit from a note identifying the member of 'X' we were comparing here and below. + // expected-error@#x {{call to deleted constructor of 'E'}} + void(X() == X()); // expected-note {{in defaulted equality comparison operator for 'X' first required here}} + + // FIXME: We would benefit from a note pointing at the selected 'operator==' here. + // expected-error@#x {{value of type 'void' is not contextually convertible to 'bool'}} + void(X() == X()); // expected-note {{in defaulted equality comparison operator for 'X' first required here}} void(X() == X()); // expected-error {{cannot be compared because its 'operator==' is implicitly deleted}} } diff --git a/clang/test/CXX/class/class.compare/class.eq/p3.cpp b/clang/test/CXX/class/class.compare/class.eq/p3.cpp new file mode 100644 index 0000000..f58b9da --- /dev/null +++ b/clang/test/CXX/class/class.compare/class.eq/p3.cpp @@ -0,0 +1,11 @@ +// RUN: %clang_cc1 -std=c++2a -verify %s + +struct A { + int a, b, c; + bool operator==(const A&) const = default; +}; + +static_assert(A{1, 2, 3} == A{1, 2, 3}); +static_assert(A{1, 2, 3} == A{0, 2, 3}); // expected-error {{failed}} +static_assert(A{1, 2, 3} == A{1, 0, 3}); // expected-error {{failed}} +static_assert(A{1, 2, 3} == A{1, 2, 0}); // expected-error {{failed}} diff --git a/clang/test/CXX/class/class.compare/class.rel/p2.cpp b/clang/test/CXX/class/class.compare/class.rel/p2.cpp index d81c963..2abe7fb 100644 --- a/clang/test/CXX/class/class.compare/class.rel/p2.cpp +++ b/clang/test/CXX/class/class.compare/class.rel/p2.cpp @@ -2,16 +2,23 @@ namespace Rel { struct A { - int operator<=>(A) const; + int n; + constexpr int operator<=>(A a) const { return n - a.n; } friend bool operator<(const A&, const A&) = default; friend bool operator<=(const A&, const A&) = default; friend bool operator>(const A&, const A&) = default; friend bool operator>=(const A&, const A&) = default; }; - bool a1 = A() < A(); - bool a2 = A() <= A(); - bool a3 = A() > A(); - bool a4 = A() >= A(); + static_assert(A{0} < A{1}); + static_assert(A{1} < A{1}); // expected-error {{failed}} + static_assert(A{0} <= A{1}); + static_assert(A{1} <= A{1}); + static_assert(A{2} <= A{1}); // expected-error {{failed}} + static_assert(A{1} > A{0}); + static_assert(A{1} > A{1}); // expected-error {{failed}} + static_assert(A{1} >= A{0}); + static_assert(A{1} >= A{1}); + static_assert(A{1} >= A{2}); // expected-error {{failed}} struct B { bool operator<=>(B) const = delete; // expected-note 4{{deleted here}} expected-note-re 8{{candidate {{.*}} deleted}} @@ -37,10 +44,12 @@ namespace Rel { // Under P2002R0, operator!= follows these rules too. namespace NotEqual { struct A { - bool operator==(A) const; + int n; + constexpr bool operator==(A a) const { return n == a.n; } friend bool operator!=(const A&, const A&) = default; }; - bool a = A() != A(); + static_assert(A{1} != A{2}); + static_assert(A{1} != A{1}); // expected-error {{failed}} struct B { bool operator==(B) const = delete; // expected-note {{deleted here}} expected-note-re 2{{candidate {{.*}} deleted}} diff --git a/clang/test/CXX/class/class.compare/class.spaceship/p1.cpp b/clang/test/CXX/class/class.compare/class.spaceship/p1.cpp index 928fe20..fafa99f 100644 --- a/clang/test/CXX/class/class.compare/class.spaceship/p1.cpp +++ b/clang/test/CXX/class/class.compare/class.spaceship/p1.cpp @@ -1,13 +1,39 @@ -// RUN: %clang_cc1 -std=c++2a -verify %s +// RUN: %clang_cc1 -std=c++2a -verify %s -fcxx-exceptions namespace std { - struct strong_ordering { + struct strong_ordering { // expected-note 3{{candidate}} int n; constexpr operator int() const { return n; } static const strong_ordering less, equal, greater; }; constexpr strong_ordering strong_ordering::less{-1}, strong_ordering::equal{0}, strong_ordering::greater{1}; + + struct weak_ordering { + int n; + constexpr weak_ordering(int n) : n(n) {} + constexpr weak_ordering(strong_ordering o) : n(o.n) {} + constexpr operator int() const { return n; } + static const weak_ordering less, equivalent, greater; + }; + constexpr weak_ordering weak_ordering::less{-1}, + weak_ordering::equivalent{0}, weak_ordering::greater{1}; + + struct partial_ordering { + double d; + constexpr partial_ordering(double d) : d(d) {} + constexpr partial_ordering(strong_ordering o) : d(o.n) {} + constexpr partial_ordering(weak_ordering o) : d(o.n) {} + constexpr operator double() const { return d; } + static const partial_ordering less, equivalent, greater, unordered; + }; + constexpr partial_ordering partial_ordering::less{-1}, + partial_ordering::equivalent{0}, partial_ordering::greater{1}, + partial_ordering::unordered{__builtin_nan("")}; + + static_assert(!(partial_ordering::unordered < 0)); + static_assert(!(partial_ordering::unordered == 0)); + static_assert(!(partial_ordering::unordered > 0)); } namespace Deletedness { @@ -59,7 +85,7 @@ namespace Deletedness { // expected-note@#base {{deleted comparison function for base class 'E'}} // expected-note@#base {{implied comparison for base class 'F' is ambiguous}} template struct Cmp : T { // #base - std::strong_ordering operator<=>(const Cmp&) const = default; // expected-note 5{{here}} + std::strong_ordering operator<=>(const Cmp&) const = default; // #cmp expected-note 5{{here}} }; void use(...); @@ -72,10 +98,80 @@ namespace Deletedness { Cmp() <=> Cmp(), // expected-error {{deleted}} Cmp() <=> Cmp(), // expected-error {{deleted}} Cmp() <=> Cmp(), // expected-error {{deleted}} - Cmp() <=> Cmp(), // FIXME: ok but synthesized body is ill-formed - Cmp() <=> Cmp(), // FIXME: ok but synthesized body is ill-formed - Cmp() <=> Cmp(), // FIXME: ok but synthesized body is ill-formed + // FIXME: The following three errors are not very good. + // expected-error@#cmp {{value of type 'void' is not contextually convertible to 'bool'}} + Cmp() <=> Cmp(), // expected-note-re {{in defaulted three-way comparison operator for '{{.*}}Cmp<{{.*}}G1>' first required here}}j + // expected-error@#cmp {{value of type 'void' is not contextually convertible to 'bool'}} + Cmp() <=> Cmp(), // expected-note-re {{in defaulted three-way comparison operator for '{{.*}}Cmp<{{.*}}G2>' first required here}}j + // expected-error@#cmp {{no matching conversion for static_cast from 'void' to 'std::strong_ordering'}} + Cmp() <=> Cmp(), // expected-note-re {{in defaulted three-way comparison operator for '{{.*}}Cmp<{{.*}}H>' first required here}}j 0 ); } } + +namespace Synthesis { + enum Result { False, True, Mu }; + + constexpr bool toBool(Result R) { + if (R == Mu) throw "should not ask this question"; + return R == True; + } + + struct Val { + Result equal, less; + constexpr bool operator==(const Val&) const { return toBool(equal); } + constexpr bool operator<(const Val&) const { return toBool(less); } + }; + + template struct Cmp { + Val val; + friend T operator<=>(const Cmp&, const Cmp&) = default; // expected-note {{deleted}} + }; + + template constexpr auto cmp(Result equal, Result less = Mu, Result reverse_less = Mu) { + return Cmp{equal, less} <=> Cmp{Mu, reverse_less}; + } + + static_assert(cmp(True) == 0); + static_assert(cmp(False, True) < 0); + static_assert(cmp(False, False) > 0); + + static_assert(cmp(True) == 0); + static_assert(cmp(False, True) < 0); + static_assert(cmp(False, False) > 0); + + static_assert(cmp(True) == 0); + static_assert(cmp(False, True) < 0); + static_assert(cmp(False, False, True) > 0); + static_assert(!(cmp(False, False, False) > 0)); + static_assert(!(cmp(False, False, False) == 0)); + static_assert(!(cmp(False, False, False) < 0)); + + // No synthesis is performed for a custom return type, even if it can be + // converted from a standard ordering. + struct custom_ordering { + custom_ordering(std::strong_ordering o); + }; + void f(Cmp c) { + c <=> c; // expected-error {{deleted}} + } +} + +namespace Preference { + struct A { + A(const A&) = delete; // expected-note {{deleted}} + // "usable" candidate that can't actually be called + friend void operator<=>(A, A); // expected-note {{passing}} + // Callable candidates for synthesis not considered. + friend bool operator==(A, A); + friend bool operator<(A, A); + }; + + struct B { + B(); + A a; + std::strong_ordering operator<=>(const B&) const = default; // expected-error {{call to deleted constructor of 'Preference::A'}} + }; + bool x = B() < B(); // expected-note {{in defaulted three-way comparison operator for 'Preference::B' first required here}} +} diff --git a/clang/test/CXX/class/class.compare/class.spaceship/p3.cpp b/clang/test/CXX/class/class.compare/class.spaceship/p3.cpp new file mode 100644 index 0000000..e4be892 --- /dev/null +++ b/clang/test/CXX/class/class.compare/class.spaceship/p3.cpp @@ -0,0 +1,35 @@ +// RUN: %clang_cc1 -std=c++2a -verify %s + +namespace std { + struct strong_ordering { + int n; + constexpr operator int() const { return n; } + static const strong_ordering less, equal, greater; + }; + constexpr strong_ordering strong_ordering::less{-1}, strong_ordering::equal{0}, strong_ordering::greater{1}; +} + +struct A { + int a, b, c; + std::strong_ordering operator<=>(const A&) const = default; +}; + +static_assert(A{1, 2, 3} <= A{1, 2, 3}); +static_assert(A{1, 2, 3} <= A{0, 20, 3}); // expected-error {{failed}} +static_assert(A{1, 2, 3} <= A{1, 0, 30}); // expected-error {{failed}} +static_assert(A{1, 2, 3} <= A{1, 2, 0}); // expected-error {{failed}} + +struct reverse_compare { + int n; + constexpr explicit reverse_compare(std::strong_ordering o) : n(-o.n) {} + constexpr operator int() const { return n; } +}; + +struct B { + int a, b, c; + friend reverse_compare operator<=>(const B&, const B&) = default; +}; +static_assert(B{1, 2, 3} >= B{1, 2, 3}); +static_assert(B{1, 2, 3} >= B{0, 20, 3}); // expected-error {{failed}} +static_assert(B{1, 2, 3} >= B{1, 0, 30}); // expected-error {{failed}} +static_assert(B{1, 2, 3} >= B{1, 2, 0}); // expected-error {{failed}} -- 2.7.4