Changeset View
Standalone View
clang/lib/Sema/SemaCoroutine.cpp
Show First 20 Lines • Show All 948 Lines • ▼ Show 20 Lines | |||||
StmtResult Sema::ActOnCoreturnStmt(Scope *S, SourceLocation Loc, Expr *E) { | StmtResult Sema::ActOnCoreturnStmt(Scope *S, SourceLocation Loc, Expr *E) { | ||||
if (!ActOnCoroutineBodyStart(S, Loc, "co_return")) { | if (!ActOnCoroutineBodyStart(S, Loc, "co_return")) { | ||||
CorrectDelayedTyposInExpr(E); | CorrectDelayedTyposInExpr(E); | ||||
return StmtError(); | return StmtError(); | ||||
} | } | ||||
return BuildCoreturnStmt(Loc, E); | return BuildCoreturnStmt(Loc, E); | ||||
} | } | ||||
// [class.copy.elision]p3: "If the expression in a [...] co_return statement | |||||
// is a (possibly parenthesized) id-expression that names an implicitly | |||||
// movable entity declared in the body or parameter-declaration-clause of | |||||
// the innermost enclosing function or lambda-expression, [...] overload | |||||
// resolution to select [...] the return_value overload to call is first | |||||
// performed as if the expression or operand were an rvalue. If the first | |||||
// overload resolution fails or was not performed, overload resolution is | |||||
// performed again, considering the expression or operand as an lvalue." | |||||
// | |||||
// Our implementation deviates from the standard in two ways: | |||||
// 1) We only consider cases where return_value is a function or function | |||||
// template, so we never try implicitly moving in the case of a function | |||||
// pointer or object of class type with operator() overload. | |||||
// 2) We don't fall back if overload resolution failed due to ambiguity, only | |||||
// if no candidate works. One could say that we do overload resolution on | |||||
// both (const) T& and T&& while treating candidates for T&& as better fit: | |||||
// so no candidate for T&& falls back to T&, but multiple equivalent | |||||
// candidates are an error. | |||||
static bool shouldMoveReturnExpr(Sema &S, RecordDecl *PromiseTypeDecl, | |||||
Expr *E) { | |||||
// Check if we're allowed to implicitly move. | |||||
VarDecl *ImplicitMoveCandidate = | |||||
S.getCopyElisionCandidate(E->getType(), E, Sema::CES_Default); | |||||
if (!ImplicitMoveCandidate) | |||||
return false; | |||||
if (ImplicitMoveCandidate->getType()->isLValueReferenceType()) | |||||
return false; | |||||
ImplicitCastExpr AsRvalue(ImplicitCastExpr::OnStack, E->getType(), CK_NoOp, E, | |||||
VK_XValue, FPOptionsOverride()); | |||||
// Lookup return_value methods in the promise type, and check if any accepts | |||||
// the moved expression. If it does, we do the move. | |||||
DeclarationNameInfo NameInfo(&S.PP.getIdentifierTable().get("return_value"), | |||||
SourceLocation{}); | |||||
LookupResult LR(S, NameInfo, Sema::LookupMemberName); | |||||
CXXScopeSpec SS; | |||||
S.LookupQualifiedName(LR, PromiseTypeDecl, SS); | |||||
for (NamedDecl *D : LR) { | |||||
if (const auto *USD = dyn_cast<UsingShadowDecl>(D)) | |||||
D = USD->getTargetDecl(); | |||||
if (const auto *CMD = dyn_cast<CXXMethodDecl>(D)) { | |||||
if (CMD->isDeleted()) | |||||
aaronpuchert: Overlad resolution can actually still fail if there is a viable candidate, namely when there… | |||||
Not Done ReplyInline ActionsThat is an interesting point! I had not considered it during P1155. I imagine that it might make implementation of P1155's new logic more difficult. GCC 8 (but not trunk) implements the behavior I would expect to see for derived-to-base conversions: https://godbolt.org/z/fj_lNw C++ always treats "an embarrassment of riches" equivalently to a "famine"; overload resolution can fail due to ambiguity just as easily as it can fail due to no candidates at all. I agree it's "a bit weird," but it would be vastly weirder if C++ did anything different from its usual behavior in this case. I'm still amenable to the idea that co_return should simply not do the copy-elision or implicit-move optimizations at all. I wish I knew some use-cases for co_return, so that we could see if the optimization is useful to anyone. Quuxplusone: That is an interesting point! I had not considered it during [P1155](https://wg21.link/p1155r2). | |||||
Not Done ReplyInline Actions
I would find it more natural to throw an error, i.e. not do the fallback, in the case of ambiguity. So the fallback should in my opinion only happen when there are no viable overload candidates, not when there are too many. I see your construction as follows: we add both operations that take an lvalue and those that take an rvalue to a bigger “overload set”, and order those that take rvalues as higher/better than those that don't. One could say that we do overload resolution on a T&& argument where we allow binding a T&& to a T&, but this is less preferable in the overload ordering. aaronpuchert: > I agree it's "a bit weird," but it would be vastly weirder if C++ did anything different from… | |||||
In light of your comment
could we re-evaluate this point? I'm not sure what your paper is going to say, but I think I agree that the two-pass mechanism is weird. aaronpuchert: In light of your comment
>>! In D88220#2435959, @Quuxplusone wrote:
> This example will be… | |||||
continue; | |||||
if (CMD->getNumParams() < 1) | |||||
continue; | |||||
for (unsigned param = 1; param < CMD->getNumParams(); ++param) | |||||
if (!CMD->getParamDecl(param)->hasDefaultArg()) | |||||
continue; | |||||
InitializedEntity Entity = InitializedEntity::InitializeParameter( | |||||
S.Context, CMD->getParamDecl(0)); | |||||
if (S.CanPerformCopyInitialization(Entity, &AsRvalue)) | |||||
return true; | |||||
} else if (auto *FTD = dyn_cast<FunctionTemplateDecl>(D)) { | |||||
TemplateDeductionInfo Info(SourceLocation{}); | |||||
FunctionDecl *Specialization = nullptr; | |||||
I should add a test case where this is necessary. aaronpuchert: I should add a test case where this is necessary. | |||||
Sema::TemplateDeductionResult Result = S.DeduceTemplateArguments( | |||||
FTD, {}, {&AsRvalue}, Specialization, Info, false, | |||||
[&](ArrayRef<QualType> ParamTypes) { | |||||
QualType ParamType = ParamTypes[0]; | |||||
if (!ParamType->isDependentType()) { | |||||
InitializedEntity Entity = InitializedEntity::InitializeParameter( | |||||
S.Context, ParamType, false); | |||||
if (!S.CanPerformCopyInitialization(Entity, &AsRvalue)) | |||||
return true; | |||||
} | |||||
return false; | |||||
}); | |||||
if (Result == Sema::TDK_Success) | |||||
return true; | |||||
} | |||||
// TODO: handle VarDecls. They can be function pointers or function objects. | |||||
} | |||||
return false; | |||||
} | |||||
StmtResult Sema::BuildCoreturnStmt(SourceLocation Loc, Expr *E, | StmtResult Sema::BuildCoreturnStmt(SourceLocation Loc, Expr *E, | ||||
bool IsImplicit) { | bool IsImplicit) { | ||||
auto *FSI = checkCoroutineContext(*this, Loc, "co_return", IsImplicit); | auto *FSI = checkCoroutineContext(*this, Loc, "co_return", IsImplicit); | ||||
if (!FSI) | if (!FSI) | ||||
return StmtError(); | return StmtError(); | ||||
if (E && E->getType()->isPlaceholderType() && | if (E && E->getType()->isPlaceholderType() && | ||||
!E->getType()->isSpecificPlaceholderType(BuiltinType::Overload)) { | !E->getType()->isSpecificPlaceholderType(BuiltinType::Overload)) { | ||||
ExprResult R = CheckPlaceholderExpr(E); | ExprResult R = CheckPlaceholderExpr(E); | ||||
if (R.isInvalid()) return StmtError(); | if (R.isInvalid()) return StmtError(); | ||||
E = R.get(); | E = R.get(); | ||||
} | } | ||||
// Move the return value if we can | |||||
if (E) { | |||||
auto NRVOCandidate = this->getCopyElisionCandidate(E->getType(), E, CES_AsIfByStdMove); | |||||
if (NRVOCandidate) { | |||||
InitializedEntity Entity = | |||||
InitializedEntity::InitializeResult(Loc, E->getType(), NRVOCandidate); | |||||
ExprResult MoveResult = this->PerformMoveOrCopyInitialization( | |||||
Entity, NRVOCandidate, E->getType(), E); | |||||
if (MoveResult.get()) | |||||
E = MoveResult.get(); | |||||
} | |||||
} | |||||
// FIXME: If the operand is a reference to a variable that's about to go out | |||||
// of scope, we should treat the operand as an xvalue for this overload | |||||
// resolution. | |||||
VarDecl *Promise = FSI->CoroutinePromise; | VarDecl *Promise = FSI->CoroutinePromise; | ||||
Should be renamed to RVOCandidate, I don't think NRVO can happen here. aaronpuchert: Should be renamed to `RVOCandidate`, I don't think NRVO can happen here. | |||||
(Btw, I have no comment on the actual code change in this patch. I'm here in my capacity as RVO-explainer-and-P1155-author, not code-understander. ;)) What's happening here is never technically "RVO" at all, because there is no "RV". However, the "N" is accurate. (See my acronym glossary for details.) The actual optimization that is happening here is "implicit move." I would strongly prefer to keep the name NRVOCandidate here, because that is the name that is used for the exact same purpose — computing "implicit move" candidates — in BuildReturnStmt. Ideally this code would be factored out so that it appeared in only one place; but until then, gratuitous differences between the two sites should be minimized IMO. Quuxplusone: (Btw, I have no comment on the actual code change in this patch. I'm here in my capacity as… | |||||
Hmm, you're completely right. What do you think about ImplicitMoveCandidate? Otherwise I'll stick with the current name, as you suggested.
Isn't it already factored out? I let getCopyElisionCandidate do all the heavy lifting. (Except filtering out lvalue references...) aaronpuchert: Hmm, you're completely right. What do you think about `ImplicitMoveCandidate`? Otherwise I'll… | |||||
I think that would be more correct in this case, but it wouldn't be parallel with the one in BuildReturnStmt, and I'm not sure whether it would be correct to change it over there too.
I see some parallelism in the two other places that call getCopyElisionCandidate followed by PerformMoveOrCopyInitialization. Maybe this is as factored-out as it can get, but it still looks remarkably parallel. (And I wish NRVOVariable was named NRVOCandidate in the latter case.) In BuildReturnStmt: if (RetValExp) NRVOCandidate = getCopyElisionCandidate(FnRetType, RetValExp, CES_Strict); if (!HasDependentReturnType && !RetValExp->isTypeDependent()) { // we have a non-void function with an expression, continue checking InitializedEntity Entity = InitializedEntity::InitializeResult(ReturnLoc, RetType, NRVOCandidate != nullptr); ExprResult Res = PerformMoveOrCopyInitialization(Entity, NRVOCandidate, RetType, RetValExp); In BuildCXXThrow: const VarDecl *NRVOVariable = nullptr; if (IsThrownVarInScope) NRVOVariable = getCopyElisionCandidate(QualType(), Ex, CES_Strict); InitializedEntity Entity = InitializedEntity::InitializeException( OpLoc, ExceptionObjectTy, /*NRVO=*/NRVOVariable != nullptr); ExprResult Res = PerformMoveOrCopyInitialization( Entity, NRVOVariable, QualType(), Ex, IsThrownVarInScope); Naming-wise, I also offer that David Stone's P1825 introduces the name "implicitly movable entity" for these things, and maybe we should call this variable ImplicitlyMovableEntity; however, I am not yet sure. Quuxplusone: > What do you think about `ImplicitMoveCandidate`?
I think that would be more correct in this… | |||||
Note that I'm removing the latter call with this change, and also the call to InitializedEntity::InitializeResult: we don't want to initialize anything. Return statements have to actually initialize a return value, but co_return statements only call <promise>.return_value. So I think finding the candidate is about as much as we can factor out. (Although it would be nice if it could also catch the case of lvalue references.) aaronpuchert: > I see some parallelism in the two other places that call `getCopyElisionCandidate` followed… | |||||
ExprResult PC; | ExprResult PC; | ||||
if (E && (isa<InitListExpr>(E) || !E->getType()->isVoidType())) { | if (E && (isa<InitListExpr>(E) || !E->getType()->isVoidType())) { | ||||
PC = buildPromiseCall(*this, Promise, Loc, "return_value", E); | Expr *Arg = E; | ||||
if (const RecordType *PromiseT = Promise->getType()->getAs<RecordType>()) | |||||
if (shouldMoveReturnExpr(*this, PromiseT->getDecl(), E)) | |||||
Arg = ImplicitCastExpr::Create(Context, E->getType(), CK_NoOp, E, | |||||
nullptr, VK_XValue, FPOptionsOverride()); | |||||
PC = buildPromiseCall(*this, Promise, Loc, "return_value", Arg); | |||||
} else { | } else { | ||||
E = MakeFullDiscardedValueExpr(E).get(); | E = MakeFullDiscardedValueExpr(E).get(); | ||||
PC = buildPromiseCall(*this, Promise, Loc, "return_void", None); | PC = buildPromiseCall(*this, Promise, Loc, "return_void", None); | ||||
} | } | ||||
if (PC.isInvalid()) | if (PC.isInvalid()) | ||||
return StmtError(); | return StmtError(); | ||||
Expr *PCE = ActOnFinishFullExpr(PC.get(), /*DiscardedValue*/ false).get(); | Expr *PCE = ActOnFinishFullExpr(PC.get(), /*DiscardedValue*/ false).get(); | ||||
Stmt *Res = new (Context) CoreturnStmt(Loc, E, PCE, IsImplicit); | Stmt *Res = new (Context) CoreturnStmt(Loc, E, PCE, IsImplicit); | ||||
Not Done ReplyInline ActionsAnother interesting question is whether this E should include the xvalue cast, if we did it. I have no idea what would be expected here. I'm tending towards replacing E by Arg. aaronpuchert: Another interesting question is whether this `E` should include the xvalue cast, if we did it. | |||||
return Res; | return Res; | ||||
} | } | ||||
/// Look up the std::nothrow object. | /// Look up the std::nothrow object. | ||||
static Expr *buildStdNoThrowDeclRef(Sema &S, SourceLocation Loc) { | static Expr *buildStdNoThrowDeclRef(Sema &S, SourceLocation Loc) { | ||||
NamespaceDecl *Std = S.getStdNamespace(); | NamespaceDecl *Std = S.getStdNamespace(); | ||||
assert(Std && "Should already be diagnosed"); | assert(Std && "Should already be diagnosed"); | ||||
▲ Show 20 Lines • Show All 687 Lines • Show Last 20 Lines |
Overlad resolution can actually still fail if there is a viable candidate, namely when there are multiple candidates and none is better than all others. It's a bit weird though to fall back to lvalue parameter then as if nothing happened.