[clang] [Clang] Keep lambdas in default template arguments dependent until used (PR #222541)

Akash Manna via cfe-commits cfe-commits at lists.llvm.org
Thu Sep 10 01:12:15 PDT 2026


https://github.com/akash-manna-sky created https://github.com/llvm/llvm-project/pull/222541

Fixes #176405

A lambda in the default argument of a template parameter is parsed as dependent, with its own template parameters one depth below the parameter list (#62611): it can only be finished once the default argument is substituted for a use of the template. That got lost when the parameter list belongs to a member template of a class template. Instantiating the enclosing class substitutes the default argument with just the outer levels, and `TransformLambdaExpr` then recomputes the closure's dependence from the `DeclContext`, which is the concrete specialization by now. So the generic lambda was rebuilt as non-dependent while its template parameters still sat at depth 1, and `[]<typename... U>(U...) {}()` was resolved on the spot: deduction never saw `U`, the pack was never expanded, and `BuildCXXDefaultArgExpr` asserted on a parameter that has no default. The non-pack variant shows the same problem as a bogus "couldn't infer template argument 'U'" on valid code.

Default arguments of a template parameter list being instantiated now go through `Sema::SubstTemplateParameterDefaultArgument`, which flags the `TemplateInstantiator` so that `ComputeLambdaDependency` keeps any lambda it transforms `LDK_AlwaysDependent`, the same way the alias-template case is handled. The instantiated member template's default argument then looks exactly like one parsed in a non-template class: the lambda is built and called only when the default argument is used, with the parameter list's own level present and the depths lined up. Deduction, overload resolution and default-argument building are unchanged.


>From 0ab79d6ecdddaeb0a13591c5f0c73f54632634bb Mon Sep 17 00:00:00 2001
From: Akash Manna <akash.manna.mymail at gmail.com>
Date: Thu, 10 Sep 2026 13:40:05 +0530
Subject: [PATCH] [Clang] Keep lambdas in default template arguments dependent
 until used

Fixes #176405

A lambda in the default argument of a template parameter is parsed as dependent, with its own template parameters one depth below the parameter list (#62611): it can only be finished once the default argument is substituted for a use of the template. That got lost when the parameter list belongs to a member template of a class template. Instantiating the enclosing class substitutes the default argument with just the outer levels, and TransformLambdaExpr then recomputes the closure's dependence from the DeclContext, which is the concrete specialization by now. So the generic lambda was rebuilt as non-dependent while its template parameters still sat at depth 1, and `[]<typename... U>(U...) {}()` was resolved on the spot: deduction never saw U, the pack was never expanded, and BuildCXXDefaultArgExpr asserted on a parameter that has no default. The non-pack variant shows the same problem as a bogus "couldn't infer template argument 'U'" on valid code.

Default arguments of a template parameter list being instantiated now go through Sema::SubstTemplateParameterDefaultArgument, which flags the TemplateInstantiator so that ComputeLambdaDependency keeps any lambda it transforms LDK_AlwaysDependent, the same way the alias-template case is handled. The instantiated member template's default argument then looks exactly like one parsed in a non-template class: the lambda is built and called only when the default argument is used, with the parameter list's own level present and the depths lined up. Deduction, overload resolution and default-argument building are unchanged.
---
 clang/docs/ReleaseNotes.md                    |  5 ++
 clang/include/clang/Sema/Sema.h               |  8 +++
 clang/lib/Sema/SemaTemplateInstantiate.cpp    | 23 ++++++++
 .../lib/Sema/SemaTemplateInstantiateDecl.cpp  |  8 +--
 clang/test/SemaCXX/cxx2a-template-lambdas.cpp | 52 +++++++++++++++++++
 5 files changed, 92 insertions(+), 4 deletions(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 3cca316a91d4d..97944a8870206 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -676,6 +676,11 @@ features cannot lower the translation-unit ABI level;
   class with an invalid non-static data member, such as one qualified with an
   address space. (#GH194605)
 
+- Fixed an assertion when instantiating a class template whose member template
+  has a default template argument that calls a generic lambda, e.g.
+  ``template <auto = []<typename... U>(U...) {}()> struct X;``. The lambda now
+  remains dependent until the default argument is used. (#GH176405)
+
 #### Bug Fixes to AST Handling
 
 - Fixed a non-deterministic ordering of unused local typedefs that made
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 4ff4c669a6b70..5d9f9cf683018 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -13576,6 +13576,14 @@ class Sema final : public SemaBase {
                              TemplateArgumentLoc &Output,
                              SourceLocation Loc = {},
                              const DeclarationName &Entity = {});
+
+  /// Substitute into the default argument of a template parameter as part of
+  /// instantiating its template parameter list. Lambdas within the default
+  /// argument stay dependent, as they were when parsed.
+  bool SubstTemplateParameterDefaultArgument(
+      const TemplateArgumentLoc &Input,
+      const MultiLevelTemplateArgumentList &TemplateArgs,
+      TemplateArgumentLoc &Output);
   bool
   SubstTemplateArguments(ArrayRef<TemplateArgumentLoc> Args,
                          const MultiLevelTemplateArgumentList &TemplateArgs,
diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp
index ffbe8bb0506bc..db13b05c81d1c 100644
--- a/clang/lib/Sema/SemaTemplateInstantiate.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp
@@ -1317,6 +1317,9 @@ namespace {
     // Whether to evaluate the C++20 constraints or simply substitute into them.
     bool EvaluateConstraints = true;
     bool EvaluateLambdaConstraint = false;
+    // Whether we are substituting into the default argument of a template
+    // parameter whose template parameter list is being instantiated.
+    bool InTemplateParameterDefaultArgument = false;
     // Whether Substitution was Incomplete, that is, we tried to substitute in
     // any user provided template arguments which were null.
     bool IsIncomplete = false;
@@ -1352,6 +1355,10 @@ namespace {
       return EvaluateConstraints;
     }
 
+    void setInTemplateParameterDefaultArgument(bool B) {
+      InTemplateParameterDefaultArgument = B;
+    }
+
     inline static struct ForParameterMappingSubstitution_t {
     } ForParameterMappingSubstitution;
 
@@ -1760,6 +1767,12 @@ namespace {
 
     CXXRecordDecl::LambdaDependencyKind
     ComputeLambdaDependency(LambdaScopeInfo *LSI) {
+      // A lambda in the default argument of a template parameter is dependent
+      // when parsed (it is within a template parameter list) and remains so
+      // while that parameter list is instantiated without being substituted
+      // itself, e.g. for a member template of a class being instantiated.
+      if (InTemplateParameterDefaultArgument)
+        return CXXRecordDecl::LambdaDependencyKind::LDK_AlwaysDependent;
       if (auto TypeAlias =
               TemplateInstArgsHelpers::getEnclosingTypeAliasTemplateDecl(
                   getSema());
@@ -4471,6 +4484,16 @@ bool Sema::SubstTemplateArgument(
   return Instantiator.TransformTemplateArgument(Input, Output);
 }
 
+bool Sema::SubstTemplateParameterDefaultArgument(
+    const TemplateArgumentLoc &Input,
+    const MultiLevelTemplateArgumentList &TemplateArgs,
+    TemplateArgumentLoc &Output) {
+  TemplateInstantiator Instantiator(*this, TemplateArgs, SourceLocation(),
+                                    DeclarationName());
+  Instantiator.setInTemplateParameterDefaultArgument(true);
+  return Instantiator.TransformTemplateArgument(Input, Output);
+}
+
 bool Sema::SubstTemplateArguments(
     ArrayRef<TemplateArgumentLoc> Args,
     const MultiLevelTemplateArgumentList &TemplateArgs,
diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
index 7668b75e836e4..e9336fd823129 100644
--- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
+++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp
@@ -3862,8 +3862,8 @@ Decl *TemplateDeclInstantiator::VisitTemplateTypeParmDecl(
   }
   if (D->hasDefaultArgument() && !D->defaultArgumentWasInherited()) {
     TemplateArgumentLoc Output;
-    if (!SemaRef.SubstTemplateArgument(D->getDefaultArgument(), TemplateArgs,
-                                       Output))
+    if (!SemaRef.SubstTemplateParameterDefaultArgument(D->getDefaultArgument(),
+                                                       TemplateArgs, Output))
       Inst->setDefaultArgument(SemaRef.getASTContext(), Output);
   }
 
@@ -4026,8 +4026,8 @@ Decl *TemplateDeclInstantiator::VisitNonTypeTemplateParmDecl(
     EnterExpressionEvaluationContext ConstantEvaluated(
         SemaRef, Sema::ExpressionEvaluationContext::ConstantEvaluated);
     TemplateArgumentLoc Result;
-    if (!SemaRef.SubstTemplateArgument(D->getDefaultArgument(), TemplateArgs,
-                                       Result))
+    if (!SemaRef.SubstTemplateParameterDefaultArgument(D->getDefaultArgument(),
+                                                       TemplateArgs, Result))
       Param->setDefaultArgument(SemaRef.Context, Result);
   }
 
diff --git a/clang/test/SemaCXX/cxx2a-template-lambdas.cpp b/clang/test/SemaCXX/cxx2a-template-lambdas.cpp
index 45d265e2cdc2b..2f7eb9ddcca48 100644
--- a/clang/test/SemaCXX/cxx2a-template-lambdas.cpp
+++ b/clang/test/SemaCXX/cxx2a-template-lambdas.cpp
@@ -96,6 +96,58 @@ void foo() {
   {return {};}(1);
 }
 
+}
+
+namespace GH176405 {
+template <int> struct bad {
+  template <auto = []<typename... U>(U...) {}()> struct X;
+  static int f() { return 0; }
+};
+int y = bad<0>::f();
+
+template <int N> struct S {
+  template <auto V = []<typename... U>(U...) { return sizeof...(U) + N; }()>
+  struct A { static constexpr auto value = V; };
+  template <auto V = []<typename U>(U u) { return u + N; }(41)>
+  struct B { static constexpr auto value = V; };
+  template <auto V = [](auto... x) { return sizeof...(x) + N; }(1, 2)>
+  struct C { static constexpr auto value = V; };
+  template <typename T, auto V = [] { return sizeof(T) + N; }()>
+  struct D { static constexpr auto value = V; };
+  template <typename T = decltype([]<typename... U>(U...) { return N; }())>
+  struct E { using type = T; };
+  template <auto V = []<typename... U>(U...) { return sizeof...(U) + N; }()>
+  static constexpr auto f() { return V; }
+  template <auto V = []<typename... U>(U...) { return sizeof...(U) + N; }()>
+  static constexpr auto var = V;
+  template <auto V = []<typename... U>(U...) { return sizeof...(U) + N; }()>
+  using alias = A<V>;
+};
+static_assert(S<1>::A<>::value == 1);
+static_assert(S<1>::A<5>::value == 5);
+static_assert(S<1>::B<>::value == 42);
+static_assert(S<1>::C<>::value == 3);
+static_assert(S<1>::D<int>::value == sizeof(int) + 1);
+static_assert(__is_same(S<1>::E<>::type, int));
+static_assert(S<1>::f() == 1);
+static_assert(S<1>::var<> == 1);
+static_assert(S<1>::alias<>::value == 1);
+
+template <int N> struct Outer {
+  template <int M> struct Inner {
+    template <auto V = []<typename... U>(U...) { return N + M; }()>
+    struct X { static constexpr auto value = V; };
+  };
+};
+static_assert(Outer<1>::Inner<2>::X<>::value == 3);
+
+template <int N> constexpr auto g() {
+  auto l = []<typename T = decltype([]<typename V>(V v) { return v; }(N))>() {
+    return T{};
+  };
+  return l();
+}
+static_assert(g<1>() == 0);
 }
 #endif
 



More information about the cfe-commits mailing list