[clang] [Clang][Sema] Reject template arguments not equivalent to their copies (part of P2308R1) (PR #193754)

Yanzuo Liu via cfe-commits cfe-commits at lists.llvm.org
Tue Apr 28 06:32:21 PDT 2026


https://github.com/zwuis updated https://github.com/llvm/llvm-project/pull/193754

>From a868262385c2d2594a44be3b9ccab1de64a820d0 Mon Sep 17 00:00:00 2001
From: Yanzuo Liu <zwuis at outlook.com>
Date: Thu, 23 Apr 2026 21:48:57 +0800
Subject: [PATCH 1/5] Reject template arguments not equivalent to their copies
 (part of P2308R1)

---
 clang/docs/ReleaseNotes.rst                   |  2 +
 .../clang/Basic/DiagnosticSemaKinds.td        |  5 ++
 clang/lib/AST/ASTContext.cpp                  |  2 +
 clang/lib/Sema/SemaTemplate.cpp               | 86 +++++++++++++++++--
 .../SemaTemplate/temp_arg_nontype_cxx20.cpp   | 57 ++++++++++--
 5 files changed, 134 insertions(+), 18 deletions(-)

diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 03362cf4e0f8a..957caabc89845 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -49,6 +49,8 @@ C++ Specific Potentially Breaking Changes
 - Clang now correctly rejects ``export`` declarations in module implementation
   partitions. (#GH107602)
 
+- Invalid template arguments which aren't equivalent to their copies are correctly rejected.
+
 ABI Changes in This Version
 ---------------------------
 
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index cc82bb6a51e52..fe12b2ed39591 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -5749,8 +5749,13 @@ def err_template_arg_not_object_or_func : Error<
   "non-type template argument does not refer to an object or function">;
 def err_template_arg_not_pointer_to_member_form : Error<
   "non-type template argument is not a pointer to member constant">;
+def err_template_arg_not_equivalent_to_its_copy : Error<
+  "non-type template argument is not equivalent to its copy">;
+def note_template_arg_requires_copy : Note<
+  "non-type template argument is required to be copyable">;
 def err_template_arg_invalid : Error<
   "non-type template argument '%0' is invalid">;
+
 def err_pointer_to_member_type : Error<
   "invalid use of pointer to member type after %select{.*|->*}0">;
 def err_pointer_to_member_call_drops_quals : Error<
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index abf5f8a832043..e520ef7eec0ce 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -13793,6 +13793,8 @@ ASTContext::getUnnamedGlobalConstantDecl(QualType Ty,
 TemplateParamObjectDecl *
 ASTContext::getTemplateParamObjectDecl(QualType T, const APValue &V) const {
   assert(T->isRecordType() && "template param object of unexpected type");
+  assert(!T->isInstantiationDependentType() &&
+         "instantiation-dependent types are not supported");
 
   // C++ [temp.param]p8:
   //   [...] a static storage duration object of type 'const T' [...]
diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index 7b7d43ef3234c..f468967abc89a 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -7130,6 +7130,72 @@ static bool CheckTemplateArgumentPointerToMember(
   return true;
 }
 
+// P2308R1  C++26 [temp.arg.nontype]p4:
+//   ... If, for the initialization from any candidate initializer,
+//     - ...
+//     - the initialization would cause P to not be
+//       template-argument-equivalent ([temp.type]) to v,
+//   the program is ill-formed.
+// TODO: Cache accepted APValue for performance improvement.
+static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
+                                                 QualType ParamType,
+                                                 const APValue &Value,
+                                                 SourceLocation ArgLoc) {
+  assert(ParamType->isRecordType() && "no need to check copy equivalence");
+
+  SourceLocation ParamLoc = Param->getLocation();
+
+  // Instead of creating a variable (C++26 [temp.arg.nontype]p3),
+  // create a template parameter object to represent the candidate initializer.
+  // They are equivalent during creating a copy.
+  auto *CandidateInitializer =
+      S.BuildDeclRefExpr(S.Context.getTemplateParamObjectDecl(ParamType, Value),
+                         ParamType.withConst(), VK_LValue, ArgLoc);
+  InitializationKind Kind = InitializationKind::CreateForInit(
+      ArgLoc, /*DirectInit=*/false, CandidateInitializer);
+  Expr *Inits[1] = {CandidateInitializer};
+  InitializedEntity Entity =
+      InitializedEntity::InitializeTemplateParameter(ParamType, Param);
+  InitializationSequence InitSeq(S, Entity, Kind, Inits);
+  ExprResult Result = InitSeq.Perform(S, Entity, Kind, Inits);
+  if (Result.isInvalid()) {
+    S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
+    return true;
+  }
+
+  // Fast path. Trivial copy constructor performs per-element copy.
+  if (cast<CXXConstructExpr>(Result.get())->getConstructor()->isTrivial())
+    return false;
+
+  Result = S.ActOnConstantExpression(Result.get());
+  Result = S.ActOnFinishFullExpr(AssertSuccess(Result), ArgLoc,
+                                 /*DiscardedValue=*/false,
+                                 /*IsConstexpr=*/true,
+                                 /*IsTemplateArgument=*/true);
+
+  APValue ValueAfterCopy, PreNarrowingValue;
+  Result = S.EvaluateConvertedConstantExpression(
+      AssertSuccess(Result), ParamType, ValueAfterCopy, CCEKind::TemplateArg,
+      /*RequireInt=*/false, PreNarrowingValue);
+  if (Result.isInvalid()) {
+    S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
+    return true;
+  }
+
+  llvm::FoldingSetNodeID VID, CID;
+  Value.Profile(VID);
+  ValueAfterCopy.Profile(CID);
+  if (VID == CID)
+    return false;
+
+  S.Diag(ArgLoc, diag::err_template_arg_not_equivalent_to_its_copy);
+  if (Param->getDeclName())
+    S.Diag(ParamLoc, diag::note_parameter_named_here) << Param->getDeclName();
+  else
+    S.Diag(ParamLoc, diag::note_parameter_here);
+  return true;
+}
+
 /// Check a template argument against its corresponding
 /// non-type template parameter.
 ///
@@ -7272,14 +7338,10 @@ ExprResult Sema::CheckTemplateArgument(NamedDecl *Param, QualType ParamType,
     return Arg;
   }
 
-  // Avoid making a copy when initializing a template parameter of class type
-  // from a template parameter object of the same type. This is going beyond
-  // the standard, but is required for soundness: in
-  //   template<A a> struct X { X *p; X<a> *q; };
-  // ... we need p and q to have the same type.
-  //
-  // Similarly, don't inject a call to a copy constructor when initializing
-  // from a template parameter of the same type.
+  // Fast path. A valid template parameter object is equivalent to its copy. No
+  // need to make a copy to check again when initializing a template parameter
+  // of class type from a template parameter object of the same type.
+  // TODO: Remove `ParamType->isRecordType()` in condition?
   Expr *InnerArg = DeductionArg->IgnoreParenImpCasts();
   if (ParamType->isRecordType() && isa<DeclRefExpr>(InnerArg) &&
       Context.hasSameUnqualifiedType(ParamType, InnerArg->getType())) {
@@ -7417,6 +7479,12 @@ ExprResult Sema::CheckTemplateArgument(NamedDecl *Param, QualType ParamType,
     if (Value.isAddrLabelDiff())
       return Diag(StartLoc, diag::err_non_type_template_arg_addr_label_diff);
 
+    if (ParamType->isRecordType() &&
+        !ParamType->isInstantiationDependentType() &&
+        CheckTemplateArgumentCopyEquivalence(*this, Param, ParamType, Value,
+                                             StartLoc))
+      return ExprError();
+
     if (ArgPE) {
       SugaredConverted = TemplateArgument(Arg, /*IsCanonical=*/false);
       CanonicalConverted =
@@ -7430,7 +7498,7 @@ ExprResult Sema::CheckTemplateArgument(NamedDecl *Param, QualType ParamType,
   }
 
   // These should have all been handled above using the C++17 rules.
-  assert(!ArgPE && !StrictCheck);
+  assert(!ArgPE && !StrictCheck && !ParamType->isRecordType());
 
   // C++ [temp.arg.nontype]p5:
   //   The following conversions are performed on each expression used
diff --git a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
index 8450ff037e184..463eb714fa675 100644
--- a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
+++ b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
@@ -106,30 +106,69 @@ namespace ConvertedConstant {
 }
 
 namespace CopyCounting {
-  // Make sure we don't use the copy constructor when transferring the "same"
-  // template parameter object around.
   struct A { int n; constexpr A(int n = 0) : n(n) {} constexpr A(const A &a) : n(a.n+1) {} };
-  template<A a> struct X {};
+  template<A a> struct X {}; // expected-note {{passing argument to parameter 'a' here}}
   template<A a> constexpr int f(X<a> x) { return a.n; }
 
-  static_assert(f(X<A{}>()) == 0);
+  static_assert(f(X<A{}>()) == 0); // expected-error {{non-type template argument is not equivalent to its copy}}
 
-  template<A a> struct Y { void f(); };
+  template<A a> struct Y { void f(); }; // expected-note {{passing argument to parameter 'a' here}}
   template<A a> void g(Y<a> y) { y.Y<a>::f(); }
-  void h() { constexpr A a; g<a>(Y<a>{}); }
+  void h() { constexpr A a; g<a>(Y<a>{}); }  // expected-error {{non-type template argument is not equivalent to its copy}}
 
-  template<A a> struct Z {
+  template<A a> struct Z { // expected-note 2 {{passing argument to parameter 'a' here}}
     constexpr int f() {
       constexpr A v = a; // this is {a.n+1}
       return Z<v>().f() + 1; // this is Z<{a.n+2}>
     }
   };
-  template<> struct Z<A{20}> {
+  template<> struct Z<A{20}> {  // expected-error {{non-type template argument is not equivalent to its copy}}
     constexpr int f() {
       return 32;
     }
   };
-  static_assert(Z<A{}>().f() == 42);
+  static_assert(Z<A{}>().f() == 42); // expected-error {{non-type template argument is not equivalent to its copy}}
+}
+
+namespace ConditionallyModify {
+struct A {
+  int x;
+  constexpr A(int x) : x(x) {}
+  constexpr A(const A& a) : x(a.x + (a.x > 1)) {}
+};
+template <A a> struct X {}; // expected-note {{passing argument to parameter 'a' here}}
+template struct X<1>;
+template struct X<2>; // expected-error {{non-type template argument is not equivalent to its copy}}
+}
+
+namespace CannotCopy {
+template <typename T, template <T> typename TEMPLATE>
+concept C = (TEMPLATE<{}>{}, true);
+
+struct S1 {
+  constexpr S1() = default; // expected-note {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
+  constexpr S1(S1&) = default; // expected-note {{candidate constructor not viable: 1st argument ('const S1') would lose const qualifier}}
+};
+template <S1> struct T1 {}; // expected-note {{passing argument to parameter here}} expected-note {{non-type template argument is required to be copyable}}
+template struct T1<{}>; // expected-error {{no matching constructor for initialization of 'S1'}}
+static_assert(!C<S1, T1>);
+
+struct S2 {
+  constexpr S2() = default; // expected-note {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
+  explicit constexpr S2(const S2&) = default; // expected-note {{explicit constructor is not a candidate}}
+};
+template <S2> struct T2 {}; // expected-note {{passing argument to parameter here}} expected-note {{non-type template argument is required to be copyable}}
+template struct T2<{}>; // expected-error {{no matching constructor for initialization of 'S2'}}
+static_assert(!C<S2, T2>);
+
+struct S3 {
+  constexpr S3() = default;
+  S3(const S3&) {} // expected-note {{declared here}}
+};
+template <S3> struct T3 {}; // expected-note {{non-type template argument is required to be copyable}}
+template struct T3<{}>; // expected-error {{non-type template argument is not a constant expression}} \
+                           expected-note {{non-constexpr constructor 'S3' cannot be used in a constant expression}}
+static_assert(!C<S3, T3>);
 }
 
 namespace StableAddress {

>From b976a63f20928f8d1506e69847620cfc6d16785a Mon Sep 17 00:00:00 2001
From: Yanzuo Liu <zwuis at outlook.com>
Date: Sun, 26 Apr 2026 12:22:52 +0800
Subject: [PATCH 2/5] Style and performance improvement

---
 clang/include/clang/AST/DeclCXX.h             | 13 ++++
 .../clang/Basic/DiagnosticSemaKinds.td        |  1 -
 clang/lib/AST/DeclCXX.cpp                     |  3 +-
 clang/lib/Sema/SemaTemplate.cpp               | 22 +++---
 .../SemaTemplate/temp_arg_nontype_cxx20.cpp   | 69 +++++++++++++------
 5 files changed, 73 insertions(+), 35 deletions(-)

diff --git a/clang/include/clang/AST/DeclCXX.h b/clang/include/clang/AST/DeclCXX.h
index 2af396f025c93..c90d3e74912eb 100644
--- a/clang/include/clang/AST/DeclCXX.h
+++ b/clang/include/clang/AST/DeclCXX.h
@@ -310,6 +310,11 @@ class CXXRecordDecl : public RecordDecl {
     LLVM_PREFERRED_TYPE(bool)
     unsigned HasODRHash : 1;
 
+    /// True if copying a template parameter (C++26 [temp.arg.nontype]p4) of
+    /// this type uses trivial copy constructor.
+    LLVM_PREFERRED_TYPE(bool)
+    unsigned TriviallyCopyTemplateParam : 1;
+
     /// A hash of parts of the class to help in ODR checking.
     unsigned ODRHash = 0;
 
@@ -595,6 +600,14 @@ class CXXRecordDecl : public RecordDecl {
 
   unsigned getODRHash() const;
 
+  void setTriviallyCopyTemplateParam() {
+    data().TriviallyCopyTemplateParam = true;
+  }
+
+  bool isTriviallyCopyTemplateParam() const {
+    return data().TriviallyCopyTemplateParam;
+  }
+
   /// Sets the base classes of this struct or class.
   void setBases(CXXBaseSpecifier const * const *Bases, unsigned NumBases);
 
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index fe12b2ed39591..3b47fa7a1ded3 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -5755,7 +5755,6 @@ def note_template_arg_requires_copy : Note<
   "non-type template argument is required to be copyable">;
 def err_template_arg_invalid : Error<
   "non-type template argument '%0' is invalid">;
-
 def err_pointer_to_member_type : Error<
   "invalid use of pointer to member type after %select{.*|->*}0">;
 def err_pointer_to_member_call_drops_quals : Error<
diff --git a/clang/lib/AST/DeclCXX.cpp b/clang/lib/AST/DeclCXX.cpp
index 3b9d888bb2c0a..742936da909d5 100644
--- a/clang/lib/AST/DeclCXX.cpp
+++ b/clang/lib/AST/DeclCXX.cpp
@@ -111,7 +111,8 @@ CXXRecordDecl::DefinitionData::DefinitionData(CXXRecordDecl *D)
       HasDeclaredCopyAssignmentWithConstParam(false),
       IsAnyDestructorNoReturn(false), IsHLSLIntangible(false), IsPFPType(false),
       IsLambda(false), IsParsingBaseSpecifiers(false),
-      ComputedVisibleConversions(false), HasODRHash(false), Definition(D) {}
+      ComputedVisibleConversions(false), HasODRHash(false),
+      TriviallyCopyTemplateParam(false), Definition(D) {}
 
 CXXBaseSpecifier *CXXRecordDecl::DefinitionData::getBasesSlowCase() const {
   return Bases.get(Definition->getASTContext().getExternalSource());
diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index f468967abc89a..926e74b25fa08 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -7136,13 +7136,15 @@ static bool CheckTemplateArgumentPointerToMember(
 //     - the initialization would cause P to not be
 //       template-argument-equivalent ([temp.type]) to v,
 //   the program is ill-formed.
-// TODO: Cache accepted APValue for performance improvement.
 static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
                                                  QualType ParamType,
                                                  const APValue &Value,
                                                  SourceLocation ArgLoc) {
   assert(ParamType->isRecordType() && "no need to check copy equivalence");
 
+  if (ParamType->castAsCXXRecordDecl()->isTriviallyCopyTemplateParam())
+    return false;
+
   SourceLocation ParamLoc = Param->getLocation();
 
   // Instead of creating a variable (C++26 [temp.arg.nontype]p3),
@@ -7158,14 +7160,13 @@ static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
       InitializedEntity::InitializeTemplateParameter(ParamType, Param);
   InitializationSequence InitSeq(S, Entity, Kind, Inits);
   ExprResult Result = InitSeq.Perform(S, Entity, Kind, Inits);
-  if (Result.isInvalid()) {
-    S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
-    return true;
-  }
+  if (Result.isInvalid())
+    return S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
 
-  // Fast path. Trivial copy constructor performs per-element copy.
-  if (cast<CXXConstructExpr>(Result.get())->getConstructor()->isTrivial())
+  if (cast<CXXConstructExpr>(Result.get())->getConstructor()->isTrivial()) {
+    ParamType->castAsCXXRecordDecl()->setTriviallyCopyTemplateParam();
     return false;
+  }
 
   Result = S.ActOnConstantExpression(Result.get());
   Result = S.ActOnFinishFullExpr(AssertSuccess(Result), ArgLoc,
@@ -7177,10 +7178,8 @@ static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
   Result = S.EvaluateConvertedConstantExpression(
       AssertSuccess(Result), ParamType, ValueAfterCopy, CCEKind::TemplateArg,
       /*RequireInt=*/false, PreNarrowingValue);
-  if (Result.isInvalid()) {
-    S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
-    return true;
-  }
+  if (Result.isInvalid())
+    return S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
 
   llvm::FoldingSetNodeID VID, CID;
   Value.Profile(VID);
@@ -7341,7 +7340,6 @@ ExprResult Sema::CheckTemplateArgument(NamedDecl *Param, QualType ParamType,
   // Fast path. A valid template parameter object is equivalent to its copy. No
   // need to make a copy to check again when initializing a template parameter
   // of class type from a template parameter object of the same type.
-  // TODO: Remove `ParamType->isRecordType()` in condition?
   Expr *InnerArg = DeductionArg->IgnoreParenImpCasts();
   if (ParamType->isRecordType() && isa<DeclRefExpr>(InnerArg) &&
       Context.hasSameUnqualifiedType(ParamType, InnerArg->getType())) {
diff --git a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
index 463eb714fa675..cb09c6dc7115d 100644
--- a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
+++ b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
@@ -107,27 +107,35 @@ namespace ConvertedConstant {
 
 namespace CopyCounting {
   struct A { int n; constexpr A(int n = 0) : n(n) {} constexpr A(const A &a) : n(a.n+1) {} };
-  template<A a> struct X {}; // expected-note {{passing argument to parameter 'a' here}}
+  template<A a> struct X {}; // #CopyCounting-X
   template<A a> constexpr int f(X<a> x) { return a.n; }
 
-  static_assert(f(X<A{}>()) == 0); // expected-error {{non-type template argument is not equivalent to its copy}}
+  static_assert(f(X<A{}>()) == 0);
+  // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
+  // expected-note@#CopyCounting-X {{passing argument to parameter 'a' here}}
 
-  template<A a> struct Y { void f(); }; // expected-note {{passing argument to parameter 'a' here}}
+  template<A a> struct Y { void f(); }; // #CopyCounting-Y
   template<A a> void g(Y<a> y) { y.Y<a>::f(); }
-  void h() { constexpr A a; g<a>(Y<a>{}); }  // expected-error {{non-type template argument is not equivalent to its copy}}
+  void h() { constexpr A a; g<a>(Y<a>{}); }
+  // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
+  // expected-note@#CopyCounting-Y {{passing argument to parameter 'a' here}}
 
-  template<A a> struct Z { // expected-note 2 {{passing argument to parameter 'a' here}}
+  template<A a> struct Z { // #CopyCounting-Z
     constexpr int f() {
       constexpr A v = a; // this is {a.n+1}
       return Z<v>().f() + 1; // this is Z<{a.n+2}>
     }
   };
-  template<> struct Z<A{20}> {  // expected-error {{non-type template argument is not equivalent to its copy}}
+  template<> struct Z<A{20}> {
+  // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
+  // expected-note@#CopyCounting-Z {{passing argument to parameter 'a' here}}
     constexpr int f() {
       return 32;
     }
   };
-  static_assert(Z<A{}>().f() == 42); // expected-error {{non-type template argument is not equivalent to its copy}}
+  static_assert(Z<A{}>().f() == 42);
+  // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
+  // expected-note@#CopyCounting-Z {{passing argument to parameter 'a' here}}
 }
 
 namespace ConditionallyModify {
@@ -136,9 +144,12 @@ struct A {
   constexpr A(int x) : x(x) {}
   constexpr A(const A& a) : x(a.x + (a.x > 1)) {}
 };
-template <A a> struct X {}; // expected-note {{passing argument to parameter 'a' here}}
+template <A a> // #ConditionallyModify-a
+struct X {};
 template struct X<1>;
-template struct X<2>; // expected-error {{non-type template argument is not equivalent to its copy}}
+template struct X<2>;
+// expected-error at -1 {{non-type template argument is not equivalent to its copy}}
+// expected-note@#ConditionallyModify-a {{passing argument to parameter 'a' here}}
 }
 
 namespace CannotCopy {
@@ -146,28 +157,44 @@ template <typename T, template <T> typename TEMPLATE>
 concept C = (TEMPLATE<{}>{}, true);
 
 struct S1 {
-  constexpr S1() = default; // expected-note {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
-  constexpr S1(S1&) = default; // expected-note {{candidate constructor not viable: 1st argument ('const S1') would lose const qualifier}}
+  constexpr S1() = default; // #CannotCopy-S1-default-ctor
+  constexpr S1(S1&) = default; // #CannotCopy-S1-copy-ctor
 };
-template <S1> struct T1 {}; // expected-note {{passing argument to parameter here}} expected-note {{non-type template argument is required to be copyable}}
-template struct T1<{}>; // expected-error {{no matching constructor for initialization of 'S1'}}
+template <S1> // #CannotCopy-T1-S1
+struct T1 {};
+template struct T1<{}>;
+// expected-error at -1 {{no matching constructor for initialization of 'S1'}}
+// expected-note@#CannotCopy-S1-copy-ctor {{candidate constructor not viable: 1st argument ('const S1') would lose const qualifier}}
+// expected-note@#CannotCopy-S1-default-ctor {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
+// expected-note@#CannotCopy-T1-S1 {{passing argument to parameter here}}
+// expected-note@#CannotCopy-T1-S1 {{non-type template argument is required to be copyable}}
 static_assert(!C<S1, T1>);
 
 struct S2 {
-  constexpr S2() = default; // expected-note {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
-  explicit constexpr S2(const S2&) = default; // expected-note {{explicit constructor is not a candidate}}
+  constexpr S2() = default; // #CannotCopy-S2-default-ctor
+  explicit constexpr S2(const S2&) = default; // #CannotCopy-S2-copy-ctor
 };
-template <S2> struct T2 {}; // expected-note {{passing argument to parameter here}} expected-note {{non-type template argument is required to be copyable}}
-template struct T2<{}>; // expected-error {{no matching constructor for initialization of 'S2'}}
+template <S2> // #CannotCopy-T2-S2
+struct T2 {};
+template struct T2<{}>;
+// expected-error at -1 {{no matching constructor for initialization of 'S2'}}
+// expected-note@#CannotCopy-S2-copy-ctor {{explicit constructor is not a candidate}}
+// expected-note@#CannotCopy-S2-default-ctor {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
+// expected-note@#CannotCopy-T2-S2 {{passing argument to parameter here}}
+// expected-note@#CannotCopy-T2-S2 {{non-type template argument is required to be copyable}}
 static_assert(!C<S2, T2>);
 
 struct S3 {
   constexpr S3() = default;
-  S3(const S3&) {} // expected-note {{declared here}}
+  S3(const S3&) {} // #CannotCopy-S3-copy-ctor
 };
-template <S3> struct T3 {}; // expected-note {{non-type template argument is required to be copyable}}
-template struct T3<{}>; // expected-error {{non-type template argument is not a constant expression}} \
-                           expected-note {{non-constexpr constructor 'S3' cannot be used in a constant expression}}
+template <S3> // #CannotCopy-T3-S3
+struct T3 {};
+template struct T3<{}>;
+// expected-error at -1 {{non-type template argument is not a constant expression}}
+// expected-note at -2 {{non-constexpr constructor 'S3' cannot be used in a constant expression}}
+// expected-note@#CannotCopy-S3-copy-ctor {{declared here}}
+// expected-note@#CannotCopy-T3-S3 {{non-type template argument is required to be copyable}}
 static_assert(!C<S3, T3>);
 }
 

>From 6f26dac62959dceae70643af0a98dd5bbc3a8bcc Mon Sep 17 00:00:00 2001
From: Yanzuo Liu <zwuis at outlook.com>
Date: Mon, 27 Apr 2026 22:56:17 +0800
Subject: [PATCH 3/5] Update clang/lib/Sema/SemaTemplate.cpp

Co-authored-by: Corentin Jabot <corentinjabot at gmail.com>
---
 clang/lib/Sema/SemaTemplate.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index 926e74b25fa08..145a334466110 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -7149,7 +7149,7 @@ static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
 
   // Instead of creating a variable (C++26 [temp.arg.nontype]p3),
   // create a template parameter object to represent the candidate initializer.
-  // They are equivalent during creating a copy.
+  // They are equivalent when creating a copy.
   auto *CandidateInitializer =
       S.BuildDeclRefExpr(S.Context.getTemplateParamObjectDecl(ParamType, Value),
                          ParamType.withConst(), VK_LValue, ArgLoc);

>From b36b29b897b3eea0850f7bf95ed712e8f1948ab4 Mon Sep 17 00:00:00 2001
From: Yanzuo Liu <zwuis at outlook.com>
Date: Mon, 27 Apr 2026 23:22:55 +0800
Subject: [PATCH 4/5] Improve readability in tests.

---
 .../SemaTemplate/temp_arg_nontype_cxx20.cpp   | 32 +++++++++----------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
index cb09c6dc7115d..a10a633dfc688 100644
--- a/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
+++ b/clang/test/SemaTemplate/temp_arg_nontype_cxx20.cpp
@@ -112,13 +112,13 @@ namespace CopyCounting {
 
   static_assert(f(X<A{}>()) == 0);
   // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
-  // expected-note@#CopyCounting-X {{passing argument to parameter 'a' here}}
+  //   expected-note@#CopyCounting-X {{passing argument to parameter 'a' here}}
 
   template<A a> struct Y { void f(); }; // #CopyCounting-Y
   template<A a> void g(Y<a> y) { y.Y<a>::f(); }
   void h() { constexpr A a; g<a>(Y<a>{}); }
   // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
-  // expected-note@#CopyCounting-Y {{passing argument to parameter 'a' here}}
+  //   expected-note@#CopyCounting-Y {{passing argument to parameter 'a' here}}
 
   template<A a> struct Z { // #CopyCounting-Z
     constexpr int f() {
@@ -128,14 +128,14 @@ namespace CopyCounting {
   };
   template<> struct Z<A{20}> {
   // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
-  // expected-note@#CopyCounting-Z {{passing argument to parameter 'a' here}}
+  //   expected-note@#CopyCounting-Z {{passing argument to parameter 'a' here}}
     constexpr int f() {
       return 32;
     }
   };
   static_assert(Z<A{}>().f() == 42);
   // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
-  // expected-note@#CopyCounting-Z {{passing argument to parameter 'a' here}}
+  //   expected-note@#CopyCounting-Z {{passing argument to parameter 'a' here}}
 }
 
 namespace ConditionallyModify {
@@ -149,7 +149,7 @@ struct X {};
 template struct X<1>;
 template struct X<2>;
 // expected-error at -1 {{non-type template argument is not equivalent to its copy}}
-// expected-note@#ConditionallyModify-a {{passing argument to parameter 'a' here}}
+//   expected-note@#ConditionallyModify-a {{passing argument to parameter 'a' here}}
 }
 
 namespace CannotCopy {
@@ -164,10 +164,10 @@ template <S1> // #CannotCopy-T1-S1
 struct T1 {};
 template struct T1<{}>;
 // expected-error at -1 {{no matching constructor for initialization of 'S1'}}
-// expected-note@#CannotCopy-S1-copy-ctor {{candidate constructor not viable: 1st argument ('const S1') would lose const qualifier}}
-// expected-note@#CannotCopy-S1-default-ctor {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
-// expected-note@#CannotCopy-T1-S1 {{passing argument to parameter here}}
-// expected-note@#CannotCopy-T1-S1 {{non-type template argument is required to be copyable}}
+//   expected-note@#CannotCopy-S1-copy-ctor {{candidate constructor not viable: 1st argument ('const S1') would lose const qualifier}}
+//   expected-note@#CannotCopy-S1-default-ctor {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
+//   expected-note@#CannotCopy-T1-S1 {{passing argument to parameter here}}
+//   expected-note@#CannotCopy-T1-S1 {{non-type template argument is required to be copyable}}
 static_assert(!C<S1, T1>);
 
 struct S2 {
@@ -178,10 +178,10 @@ template <S2> // #CannotCopy-T2-S2
 struct T2 {};
 template struct T2<{}>;
 // expected-error at -1 {{no matching constructor for initialization of 'S2'}}
-// expected-note@#CannotCopy-S2-copy-ctor {{explicit constructor is not a candidate}}
-// expected-note@#CannotCopy-S2-default-ctor {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
-// expected-note@#CannotCopy-T2-S2 {{passing argument to parameter here}}
-// expected-note@#CannotCopy-T2-S2 {{non-type template argument is required to be copyable}}
+//   expected-note@#CannotCopy-S2-copy-ctor {{explicit constructor is not a candidate}}
+//   expected-note@#CannotCopy-S2-default-ctor {{candidate constructor not viable: requires 0 arguments, but 1 was provided}}
+//   expected-note@#CannotCopy-T2-S2 {{passing argument to parameter here}}
+//   expected-note@#CannotCopy-T2-S2 {{non-type template argument is required to be copyable}}
 static_assert(!C<S2, T2>);
 
 struct S3 {
@@ -192,9 +192,9 @@ template <S3> // #CannotCopy-T3-S3
 struct T3 {};
 template struct T3<{}>;
 // expected-error at -1 {{non-type template argument is not a constant expression}}
-// expected-note at -2 {{non-constexpr constructor 'S3' cannot be used in a constant expression}}
-// expected-note@#CannotCopy-S3-copy-ctor {{declared here}}
-// expected-note@#CannotCopy-T3-S3 {{non-type template argument is required to be copyable}}
+//   expected-note at -2 {{non-constexpr constructor 'S3' cannot be used in a constant expression}}
+//   expected-note@#CannotCopy-S3-copy-ctor {{declared here}}
+//   expected-note@#CannotCopy-T3-S3 {{non-type template argument is required to be copyable}}
 static_assert(!C<S3, T3>);
 }
 

>From 2c62578b85f2f1526ca76dc83cf28b891f7a9e6b Mon Sep 17 00:00:00 2001
From: Yanzuo Liu <zwuis at outlook.com>
Date: Tue, 28 Apr 2026 21:26:01 +0800
Subject: [PATCH 5/5] Grammar fix and ambiguity avoidance

---
 clang/docs/ReleaseNotes.rst       |  2 +-
 clang/include/clang/AST/DeclCXX.h | 10 +++++-----
 clang/lib/AST/DeclCXX.cpp         |  2 +-
 clang/lib/Sema/SemaTemplate.cpp   |  6 +++---
 4 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 957caabc89845..599b17b5ad2ff 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -49,7 +49,7 @@ C++ Specific Potentially Breaking Changes
 - Clang now correctly rejects ``export`` declarations in module implementation
   partitions. (#GH107602)
 
-- Invalid template arguments which aren't equivalent to their copies are correctly rejected.
+- Template arguments which aren't equivalent to their copies are correctly rejected.
 
 ABI Changes in This Version
 ---------------------------
diff --git a/clang/include/clang/AST/DeclCXX.h b/clang/include/clang/AST/DeclCXX.h
index c90d3e74912eb..b2a1010b4ebec 100644
--- a/clang/include/clang/AST/DeclCXX.h
+++ b/clang/include/clang/AST/DeclCXX.h
@@ -313,7 +313,7 @@ class CXXRecordDecl : public RecordDecl {
     /// True if copying a template parameter (C++26 [temp.arg.nontype]p4) of
     /// this type uses trivial copy constructor.
     LLVM_PREFERRED_TYPE(bool)
-    unsigned TriviallyCopyTemplateParam : 1;
+    unsigned TriviallyCopyingTemplateParam : 1;
 
     /// A hash of parts of the class to help in ODR checking.
     unsigned ODRHash = 0;
@@ -600,12 +600,12 @@ class CXXRecordDecl : public RecordDecl {
 
   unsigned getODRHash() const;
 
-  void setTriviallyCopyTemplateParam() {
-    data().TriviallyCopyTemplateParam = true;
+  void setTriviallyCopyingTemplateParam() {
+    data().TriviallyCopyingTemplateParam = true;
   }
 
-  bool isTriviallyCopyTemplateParam() const {
-    return data().TriviallyCopyTemplateParam;
+  bool isTriviallyCopyingTemplateParam() const {
+    return data().TriviallyCopyingTemplateParam;
   }
 
   /// Sets the base classes of this struct or class.
diff --git a/clang/lib/AST/DeclCXX.cpp b/clang/lib/AST/DeclCXX.cpp
index 742936da909d5..43923771f1832 100644
--- a/clang/lib/AST/DeclCXX.cpp
+++ b/clang/lib/AST/DeclCXX.cpp
@@ -112,7 +112,7 @@ CXXRecordDecl::DefinitionData::DefinitionData(CXXRecordDecl *D)
       IsAnyDestructorNoReturn(false), IsHLSLIntangible(false), IsPFPType(false),
       IsLambda(false), IsParsingBaseSpecifiers(false),
       ComputedVisibleConversions(false), HasODRHash(false),
-      TriviallyCopyTemplateParam(false), Definition(D) {}
+      TriviallyCopyingTemplateParam(false), Definition(D) {}
 
 CXXBaseSpecifier *CXXRecordDecl::DefinitionData::getBasesSlowCase() const {
   return Bases.get(Definition->getASTContext().getExternalSource());
diff --git a/clang/lib/Sema/SemaTemplate.cpp b/clang/lib/Sema/SemaTemplate.cpp
index 145a334466110..b43b1db92852f 100644
--- a/clang/lib/Sema/SemaTemplate.cpp
+++ b/clang/lib/Sema/SemaTemplate.cpp
@@ -7142,7 +7142,7 @@ static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
                                                  SourceLocation ArgLoc) {
   assert(ParamType->isRecordType() && "no need to check copy equivalence");
 
-  if (ParamType->castAsCXXRecordDecl()->isTriviallyCopyTemplateParam())
+  if (ParamType->castAsCXXRecordDecl()->isTriviallyCopyingTemplateParam())
     return false;
 
   SourceLocation ParamLoc = Param->getLocation();
@@ -7164,11 +7164,11 @@ static bool CheckTemplateArgumentCopyEquivalence(Sema &S, NamedDecl *Param,
     return S.Diag(ParamLoc, diag::note_template_arg_requires_copy);
 
   if (cast<CXXConstructExpr>(Result.get())->getConstructor()->isTrivial()) {
-    ParamType->castAsCXXRecordDecl()->setTriviallyCopyTemplateParam();
+    ParamType->castAsCXXRecordDecl()->setTriviallyCopyingTemplateParam();
     return false;
   }
 
-  Result = S.ActOnConstantExpression(Result.get());
+  Result = S.ActOnConstantExpression(Result);
   Result = S.ActOnFinishFullExpr(AssertSuccess(Result), ArgLoc,
                                  /*DiscardedValue=*/false,
                                  /*IsConstexpr=*/true,



More information about the cfe-commits mailing list