forked from OSchip/llvm-project
PR42104: Support instantiations of lambdas that implicitly capture
packs. Two changes: * Track odr-use via FunctionParmPackExprs to properly handle dependent odr-uses of packs in generic lambdas. * Do not instantiate implicit captures; instead, regenerate them by instantiating the body of the lambda. This is necessary to distinguish between cases where only one element of a pack is captured and cases where the entire pack is captured. llvm-svn: 362358
This commit is contained in:
parent
ce1534b405
commit
ea0c66be55
|
@ -15,6 +15,7 @@
|
|||
#define LLVM_CLANG_SEMA_SCOPEINFO_H
|
||||
|
||||
#include "clang/AST/Expr.h"
|
||||
#include "clang/AST/ExprCXX.h"
|
||||
#include "clang/AST/Type.h"
|
||||
#include "clang/Basic/CapturedStmt.h"
|
||||
#include "clang/Basic/LLVM.h"
|
||||
|
@ -913,7 +914,8 @@ public:
|
|||
/// };
|
||||
/// }
|
||||
void addPotentialCapture(Expr *VarExpr) {
|
||||
assert(isa<DeclRefExpr>(VarExpr) || isa<MemberExpr>(VarExpr));
|
||||
assert(isa<DeclRefExpr>(VarExpr) || isa<MemberExpr>(VarExpr) ||
|
||||
isa<FunctionParmPackExpr>(VarExpr));
|
||||
PotentiallyCapturingExprs.push_back(VarExpr);
|
||||
}
|
||||
|
||||
|
@ -965,13 +967,15 @@ public:
|
|||
/// building such a node. So we need a rule that anyone can implement and get
|
||||
/// exactly the same result".
|
||||
void markVariableExprAsNonODRUsed(Expr *CapturingVarExpr) {
|
||||
assert(isa<DeclRefExpr>(CapturingVarExpr)
|
||||
|| isa<MemberExpr>(CapturingVarExpr));
|
||||
assert(isa<DeclRefExpr>(CapturingVarExpr) ||
|
||||
isa<MemberExpr>(CapturingVarExpr) ||
|
||||
isa<FunctionParmPackExpr>(CapturingVarExpr));
|
||||
NonODRUsedCapturingExprs.insert(CapturingVarExpr);
|
||||
}
|
||||
bool isVariableExprMarkedAsNonODRUsed(Expr *CapturingVarExpr) const {
|
||||
assert(isa<DeclRefExpr>(CapturingVarExpr)
|
||||
|| isa<MemberExpr>(CapturingVarExpr));
|
||||
assert(isa<DeclRefExpr>(CapturingVarExpr) ||
|
||||
isa<MemberExpr>(CapturingVarExpr) ||
|
||||
isa<FunctionParmPackExpr>(CapturingVarExpr));
|
||||
return NonODRUsedCapturingExprs.count(CapturingVarExpr);
|
||||
}
|
||||
void removePotentialCapture(Expr *E) {
|
||||
|
@ -993,9 +997,8 @@ public:
|
|||
PotentialThisCaptureLocation.isValid();
|
||||
}
|
||||
|
||||
// When passed the index, returns the VarDecl and Expr associated
|
||||
// with the index.
|
||||
void getPotentialVariableCapture(unsigned Idx, VarDecl *&VD, Expr *&E) const;
|
||||
void visitPotentialCaptures(
|
||||
llvm::function_ref<void(VarDecl *, Expr *)> Callback) const;
|
||||
};
|
||||
|
||||
FunctionScopeInfo::WeakObjectProfileTy::WeakObjectProfileTy()
|
||||
|
|
|
@ -4179,6 +4179,7 @@ public:
|
|||
void MarkVariableReferenced(SourceLocation Loc, VarDecl *Var);
|
||||
void MarkDeclRefReferenced(DeclRefExpr *E, const Expr *Base = nullptr);
|
||||
void MarkMemberReferenced(MemberExpr *E);
|
||||
void MarkFunctionParmPackReferenced(FunctionParmPackExpr *E);
|
||||
void MarkCaptureUsedInEnclosingContext(VarDecl *Capture, SourceLocation Loc,
|
||||
unsigned CapturingScopeIndex);
|
||||
|
||||
|
|
|
@ -229,20 +229,20 @@ bool CapturingScopeInfo::isVLATypeCaptured(const VariableArrayType *VAT) const {
|
|||
return false;
|
||||
}
|
||||
|
||||
void LambdaScopeInfo::getPotentialVariableCapture(unsigned Idx, VarDecl *&VD,
|
||||
Expr *&E) const {
|
||||
assert(Idx < getNumPotentialVariableCaptures() &&
|
||||
"Index of potential capture must be within 0 to less than the "
|
||||
"number of captures!");
|
||||
E = PotentiallyCapturingExprs[Idx];
|
||||
if (DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(E))
|
||||
VD = dyn_cast<VarDecl>(DRE->getFoundDecl());
|
||||
else if (MemberExpr *ME = dyn_cast<MemberExpr>(E))
|
||||
VD = dyn_cast<VarDecl>(ME->getMemberDecl());
|
||||
else
|
||||
llvm_unreachable("Only DeclRefExprs or MemberExprs should be added for "
|
||||
"potential captures");
|
||||
assert(VD);
|
||||
void LambdaScopeInfo::visitPotentialCaptures(
|
||||
llvm::function_ref<void(VarDecl *, Expr *)> Callback) const {
|
||||
for (Expr *E : PotentiallyCapturingExprs) {
|
||||
if (auto *DRE = dyn_cast<DeclRefExpr>(E)) {
|
||||
Callback(cast<VarDecl>(DRE->getFoundDecl()), E);
|
||||
} else if (auto *ME = dyn_cast<MemberExpr>(E)) {
|
||||
Callback(cast<VarDecl>(ME->getMemberDecl()), E);
|
||||
} else if (auto *FP = dyn_cast<FunctionParmPackExpr>(E)) {
|
||||
for (VarDecl *VD : *FP)
|
||||
Callback(VD, E);
|
||||
} else {
|
||||
llvm_unreachable("unexpected expression in potential captures list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FunctionScopeInfo::~FunctionScopeInfo() { }
|
||||
|
|
|
@ -14610,7 +14610,9 @@ namespace {
|
|||
// context so never needs to be transformed.
|
||||
// FIXME: Ideally we wouldn't transform the closure type either, and would
|
||||
// just recreate the capture expressions and lambda expression.
|
||||
StmtResult TransformLambdaBody(Stmt *Body) { return Body; }
|
||||
StmtResult TransformLambdaBody(LambdaExpr *E, Stmt *Body) {
|
||||
return SkipLambdaBody(E, Body);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -15054,7 +15056,7 @@ void Sema::MarkFunctionReferenced(SourceLocation Loc, FunctionDecl *Func,
|
|||
/// *FunctionScopeIndexToStopAt on the FunctionScopeInfo stack.
|
||||
static void
|
||||
MarkVarDeclODRUsed(VarDecl *Var, SourceLocation Loc, Sema &SemaRef,
|
||||
const unsigned *const FunctionScopeIndexToStopAt) {
|
||||
const unsigned *const FunctionScopeIndexToStopAt = nullptr) {
|
||||
// Keep track of used but undefined variables.
|
||||
// FIXME: We shouldn't suppress this warning for static data members.
|
||||
if (Var->hasDefinition(SemaRef.Context) == VarDecl::DeclarationOnly &&
|
||||
|
@ -15735,14 +15737,19 @@ void Sema::UpdateMarkingForLValueToRValue(Expr *E) {
|
|||
// variable.
|
||||
if (LambdaScopeInfo *LSI = getCurLambda()) {
|
||||
Expr *SansParensExpr = E->IgnoreParens();
|
||||
VarDecl *Var = nullptr;
|
||||
VarDecl *Var;
|
||||
ArrayRef<VarDecl *> Vars = None;
|
||||
if (DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(SansParensExpr))
|
||||
Var = dyn_cast<VarDecl>(DRE->getFoundDecl());
|
||||
Vars = Var = dyn_cast<VarDecl>(DRE->getFoundDecl());
|
||||
else if (MemberExpr *ME = dyn_cast<MemberExpr>(SansParensExpr))
|
||||
Var = dyn_cast<VarDecl>(ME->getMemberDecl());
|
||||
Vars = Var = dyn_cast<VarDecl>(ME->getMemberDecl());
|
||||
else if (auto *FPPE = dyn_cast<FunctionParmPackExpr>(SansParensExpr))
|
||||
Vars = llvm::makeArrayRef(FPPE->begin(), FPPE->end());
|
||||
|
||||
if (Var && IsVariableNonDependentAndAConstantExpression(Var, Context))
|
||||
LSI->markVariableExprAsNonODRUsed(SansParensExpr);
|
||||
for (VarDecl *VD : Vars) {
|
||||
if (Var && IsVariableNonDependentAndAConstantExpression(VD, Context))
|
||||
LSI->markVariableExprAsNonODRUsed(SansParensExpr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15767,20 +15774,18 @@ void Sema::CleanupVarDeclMarking() {
|
|||
std::swap(LocalMaybeODRUseExprs, MaybeODRUseExprs);
|
||||
|
||||
for (Expr *E : LocalMaybeODRUseExprs) {
|
||||
VarDecl *Var;
|
||||
SourceLocation Loc;
|
||||
if (DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(E)) {
|
||||
Var = cast<VarDecl>(DRE->getDecl());
|
||||
Loc = DRE->getLocation();
|
||||
} else if (MemberExpr *ME = dyn_cast<MemberExpr>(E)) {
|
||||
Var = cast<VarDecl>(ME->getMemberDecl());
|
||||
Loc = ME->getMemberLoc();
|
||||
if (auto *DRE = dyn_cast<DeclRefExpr>(E)) {
|
||||
MarkVarDeclODRUsed(cast<VarDecl>(DRE->getDecl()),
|
||||
DRE->getLocation(), *this);
|
||||
} else if (auto *ME = dyn_cast<MemberExpr>(E)) {
|
||||
MarkVarDeclODRUsed(cast<VarDecl>(ME->getMemberDecl()), ME->getMemberLoc(),
|
||||
*this);
|
||||
} else if (auto *FP = dyn_cast<FunctionParmPackExpr>(E)) {
|
||||
for (VarDecl *VD : *FP)
|
||||
MarkVarDeclODRUsed(VD, FP->getParameterPackLocation(), *this);
|
||||
} else {
|
||||
llvm_unreachable("Unexpected expression");
|
||||
}
|
||||
|
||||
MarkVarDeclODRUsed(Var, Loc, *this,
|
||||
/*MaxFunctionScopeIndex Pointer*/ nullptr);
|
||||
}
|
||||
|
||||
assert(MaybeODRUseExprs.empty() &&
|
||||
|
@ -15789,7 +15794,8 @@ void Sema::CleanupVarDeclMarking() {
|
|||
|
||||
static void DoMarkVarDeclReferenced(Sema &SemaRef, SourceLocation Loc,
|
||||
VarDecl *Var, Expr *E) {
|
||||
assert((!E || isa<DeclRefExpr>(E) || isa<MemberExpr>(E)) &&
|
||||
assert((!E || isa<DeclRefExpr>(E) || isa<MemberExpr>(E) ||
|
||||
isa<FunctionParmPackExpr>(E)) &&
|
||||
"Invalid Expr argument to DoMarkVarDeclReferenced");
|
||||
Var->setReferenced();
|
||||
|
||||
|
@ -16022,6 +16028,12 @@ void Sema::MarkMemberReferenced(MemberExpr *E) {
|
|||
MarkExprReferenced(*this, Loc, E->getMemberDecl(), E, MightBeOdrUse);
|
||||
}
|
||||
|
||||
/// Perform reference-marking and odr-use handling for a FunctionParmPackExpr.
|
||||
void Sema::MarkFunctionParmPackReferenced(FunctionParmPackExpr *E) {
|
||||
for (VarDecl *VD : *E)
|
||||
MarkExprReferenced(*this, E->getParameterPackLocation(), VD, E, true);
|
||||
}
|
||||
|
||||
/// Perform marking for a reference to an arbitrary declaration. It
|
||||
/// marks the declaration referenced, and performs odr-use checking for
|
||||
/// functions and variables. This method should not be used when building a
|
||||
|
|
|
@ -7427,12 +7427,7 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
|
|||
// All the potentially captureable variables in the current nested
|
||||
// lambda (within a generic outer lambda), must be captured by an
|
||||
// outer lambda that is enclosed within a non-dependent context.
|
||||
const unsigned NumPotentialCaptures =
|
||||
CurrentLSI->getNumPotentialVariableCaptures();
|
||||
for (unsigned I = 0; I != NumPotentialCaptures; ++I) {
|
||||
Expr *VarExpr = nullptr;
|
||||
VarDecl *Var = nullptr;
|
||||
CurrentLSI->getPotentialVariableCapture(I, Var, VarExpr);
|
||||
CurrentLSI->visitPotentialCaptures([&] (VarDecl *Var, Expr *VarExpr) {
|
||||
// If the variable is clearly identified as non-odr-used and the full
|
||||
// expression is not instantiation dependent, only then do we not
|
||||
// need to check enclosing lambda's for speculative captures.
|
||||
|
@ -7446,7 +7441,7 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
|
|||
// }
|
||||
if (CurrentLSI->isVariableExprMarkedAsNonODRUsed(VarExpr) &&
|
||||
!IsFullExprInstantiationDependent)
|
||||
continue;
|
||||
return;
|
||||
|
||||
// If we have a capture-capable lambda for the variable, go ahead and
|
||||
// capture the variable in that lambda (and all its enclosing lambdas).
|
||||
|
@ -7478,7 +7473,7 @@ static void CheckIfAnyEnclosingLambdasMustCaptureAnyPotentialCaptures(
|
|||
DeclRefType, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check if 'this' needs to be captured.
|
||||
if (CurrentLSI->hasPotentialThisCapture()) {
|
||||
|
|
|
@ -1368,9 +1368,11 @@ TemplateInstantiator::TransformFunctionParmPackExpr(FunctionParmPackExpr *E) {
|
|||
Vars.push_back(D);
|
||||
}
|
||||
|
||||
return FunctionParmPackExpr::Create(getSema().Context, T,
|
||||
E->getParameterPack(),
|
||||
E->getParameterPackLocation(), Vars);
|
||||
auto *PackExpr =
|
||||
FunctionParmPackExpr::Create(getSema().Context, T, E->getParameterPack(),
|
||||
E->getParameterPackLocation(), Vars);
|
||||
getSema().MarkFunctionParmPackReferenced(PackExpr);
|
||||
return PackExpr;
|
||||
}
|
||||
|
||||
ExprResult
|
||||
|
@ -1389,8 +1391,10 @@ TemplateInstantiator::TransformFunctionParmPackRefExpr(DeclRefExpr *E,
|
|||
QualType T = TransformType(E->getType());
|
||||
if (T.isNull())
|
||||
return ExprError();
|
||||
return FunctionParmPackExpr::Create(getSema().Context, T, PD,
|
||||
E->getExprLoc(), *Pack);
|
||||
auto *PackExpr = FunctionParmPackExpr::Create(getSema().Context, T, PD,
|
||||
E->getExprLoc(), *Pack);
|
||||
getSema().MarkFunctionParmPackReferenced(PackExpr);
|
||||
return PackExpr;
|
||||
}
|
||||
|
||||
TransformedDecl = (*Pack)[getSema().ArgumentPackSubstitutionIndex];
|
||||
|
|
|
@ -660,7 +660,10 @@ public:
|
|||
bool ExpectParameterPack);
|
||||
|
||||
/// Transform the body of a lambda-expression.
|
||||
StmtResult TransformLambdaBody(Stmt *Body);
|
||||
StmtResult TransformLambdaBody(LambdaExpr *E, Stmt *Body);
|
||||
/// Alternative implementation of TransformLambdaBody that skips transforming
|
||||
/// the body.
|
||||
StmtResult SkipLambdaBody(LambdaExpr *E, Stmt *Body);
|
||||
|
||||
QualType TransformReferenceType(TypeLocBuilder &TLB, ReferenceTypeLoc TL);
|
||||
|
||||
|
@ -11358,16 +11361,13 @@ TreeTransform<Derived>::TransformLambdaExpr(LambdaExpr *E) {
|
|||
bool Invalid = false;
|
||||
|
||||
// Transform captures.
|
||||
bool FinishedExplicitCaptures = false;
|
||||
for (LambdaExpr::capture_iterator C = E->capture_begin(),
|
||||
CEnd = E->capture_end();
|
||||
C != CEnd; ++C) {
|
||||
// When we hit the first implicit capture, tell Sema that we've finished
|
||||
// the list of explicit captures.
|
||||
if (!FinishedExplicitCaptures && C->isImplicit()) {
|
||||
getSema().finishLambdaExplicitCaptures(LSI);
|
||||
FinishedExplicitCaptures = true;
|
||||
}
|
||||
if (C->isImplicit())
|
||||
break;
|
||||
|
||||
// Capturing 'this' is trivial.
|
||||
if (C->capturesThis()) {
|
||||
|
@ -11476,17 +11476,16 @@ TreeTransform<Derived>::TransformLambdaExpr(LambdaExpr *E) {
|
|||
getSema().tryCaptureVariable(CapturedVar, C->getLocation(), Kind,
|
||||
EllipsisLoc);
|
||||
}
|
||||
if (!FinishedExplicitCaptures)
|
||||
getSema().finishLambdaExplicitCaptures(LSI);
|
||||
getSema().finishLambdaExplicitCaptures(LSI);
|
||||
|
||||
// Enter a new evaluation context to insulate the lambda from any
|
||||
// cleanups from the enclosing full-expression.
|
||||
// FIXME: Sema's lambda-building mechanism expects us to push an expression
|
||||
// evaluation context even if we're not transforming the function body.
|
||||
getSema().PushExpressionEvaluationContext(
|
||||
Sema::ExpressionEvaluationContext::PotentiallyEvaluated);
|
||||
|
||||
// Instantiate the body of the lambda expression.
|
||||
StmtResult Body =
|
||||
Invalid ? StmtError() : getDerived().TransformLambdaBody(E->getBody());
|
||||
Invalid ? StmtError() : getDerived().TransformLambdaBody(E, E->getBody());
|
||||
|
||||
// ActOnLambda* will pop the function scope for us.
|
||||
FuncScopeCleanup.disable();
|
||||
|
@ -11512,10 +11511,50 @@ TreeTransform<Derived>::TransformLambdaExpr(LambdaExpr *E) {
|
|||
|
||||
template<typename Derived>
|
||||
StmtResult
|
||||
TreeTransform<Derived>::TransformLambdaBody(Stmt *S) {
|
||||
TreeTransform<Derived>::TransformLambdaBody(LambdaExpr *E, Stmt *S) {
|
||||
return TransformStmt(S);
|
||||
}
|
||||
|
||||
template<typename Derived>
|
||||
StmtResult
|
||||
TreeTransform<Derived>::SkipLambdaBody(LambdaExpr *E, Stmt *S) {
|
||||
// Transform captures.
|
||||
for (LambdaExpr::capture_iterator C = E->capture_begin(),
|
||||
CEnd = E->capture_end();
|
||||
C != CEnd; ++C) {
|
||||
// When we hit the first implicit capture, tell Sema that we've finished
|
||||
// the list of explicit captures.
|
||||
if (!C->isImplicit())
|
||||
continue;
|
||||
|
||||
// Capturing 'this' is trivial.
|
||||
if (C->capturesThis()) {
|
||||
getSema().CheckCXXThisCapture(C->getLocation(), C->isExplicit(),
|
||||
/*BuildAndDiagnose*/ true, nullptr,
|
||||
C->getCaptureKind() == LCK_StarThis);
|
||||
continue;
|
||||
}
|
||||
// Captured expression will be recaptured during captured variables
|
||||
// rebuilding.
|
||||
if (C->capturesVLAType())
|
||||
continue;
|
||||
|
||||
assert(C->capturesVariable() && "unexpected kind of lambda capture");
|
||||
assert(!E->isInitCapture(C) && "implicit init-capture?");
|
||||
|
||||
// Transform the captured variable.
|
||||
VarDecl *CapturedVar = cast_or_null<VarDecl>(
|
||||
getDerived().TransformDecl(C->getLocation(), C->getCapturedVar()));
|
||||
if (!CapturedVar || CapturedVar->isInvalidDecl())
|
||||
return StmtError();
|
||||
|
||||
// Capture the transformed variable.
|
||||
getSema().tryCaptureVariable(CapturedVar, C->getLocation());
|
||||
}
|
||||
|
||||
return S;
|
||||
}
|
||||
|
||||
template<typename Derived>
|
||||
ExprResult
|
||||
TreeTransform<Derived>::TransformCXXUnresolvedConstructExpr(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// RUN: %clang_cc1 -std=c++1y -verify -fsyntax-only -fblocks -emit-llvm-only %s
|
||||
// RUN: %clang_cc1 -std=c++2a -verify -fsyntax-only -fblocks -emit-llvm-only %s
|
||||
// DONTRUNYET: %clang_cc1 -std=c++1y -verify -fsyntax-only -fblocks -fdelayed-template-parsing %s -DDELAYED_TEMPLATE_PARSING
|
||||
// DONTRUNYET: %clang_cc1 -std=c++1y -verify -fsyntax-only -fblocks -fms-extensions %s -DMS_EXTENSIONS
|
||||
// DONTRUNYET: %clang_cc1 -std=c++1y -verify -fsyntax-only -fblocks -fdelayed-template-parsing -fms-extensions %s -DMS_EXTENSIONS -DDELAYED_TEMPLATE_PARSING
|
||||
|
@ -176,7 +177,13 @@ void doit() {
|
|||
sample::X cx{5};
|
||||
auto L = [=](auto a) {
|
||||
const int z = 3;
|
||||
// FIXME: The warning below is correct but for some reason doesn't show
|
||||
// up in C++17 mode.
|
||||
return [&,a](auto b) {
|
||||
#if __cplusplus > 201702L
|
||||
// expected-warning@-2 {{address of stack memory associated with local variable 'z' returned}}
|
||||
// expected-note@#call {{in instantiation of}}
|
||||
#endif
|
||||
const int y = 5;
|
||||
return [=](auto c) {
|
||||
int d[sizeof(a) == sizeof(c) || sizeof(c) == sizeof(b) ? 2 : 1];
|
||||
|
@ -189,7 +196,7 @@ void doit() {
|
|||
};
|
||||
};
|
||||
};
|
||||
auto M = L(3)(3.5);
|
||||
auto M = L(3)(3.5); // #call
|
||||
M(3.14);
|
||||
}
|
||||
}
|
||||
|
@ -1519,6 +1526,20 @@ void test() {
|
|||
|
||||
} // end ns5
|
||||
|
||||
|
||||
|
||||
} // end PR34266
|
||||
|
||||
namespace capture_pack {
|
||||
#if __cplusplus >= 201702L
|
||||
constexpr
|
||||
#endif
|
||||
auto v =
|
||||
[](auto ...a) {
|
||||
[&](auto ...b) {
|
||||
((a = b), ...); // expected-warning 0-1{{extension}}
|
||||
}(100, 20, 3);
|
||||
return (a + ...); // expected-warning 0-1{{extension}}
|
||||
}(400, 50, 6);
|
||||
#if __cplusplus >= 201702L
|
||||
static_assert(v == 123);
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// RUN: %clang_cc1 -std=c++2a -verify %s
|
||||
// expected-no-diagnostics
|
||||
|
||||
template<typename ...T, typename ...Lambda> void check_sizes(Lambda ...L) {
|
||||
static_assert(((sizeof(T) == sizeof(Lambda)) && ...));
|
||||
}
|
||||
|
||||
template<typename ...T> void f(T ...v) {
|
||||
// Pack expansion of lambdas: each lambda captures only one pack element.
|
||||
check_sizes<T...>([=] { (void)&v; } ...);
|
||||
|
||||
// Pack expansion inside lambda: captures all pack elements.
|
||||
auto l = [=] { ((void)&v, ...); };
|
||||
static_assert(sizeof(l) >= (sizeof(T) + ...));
|
||||
}
|
||||
|
||||
template void f(int, char, double);
|
Loading…
Reference in New Issue