[clang] [clang] Fix partial specialization matching for NTTPs of placeholder type (PR #225239)
Valentyn Yukhymenko via cfe-commits
cfe-commits at lists.llvm.org
Wed Sep 23 14:57:25 PDT 2026
https://github.com/BaLiKfromUA updated https://github.com/llvm/llvm-project/pull/225239
>From 368757fd84d130c0ed250efa489cb56e8a3e2449 Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Tue, 22 Sep 2026 01:01:23 +0100
Subject: [PATCH 1/4] [clang] Fix partial specialization matching for NTTPs of
placeholder type
---
clang/docs/ReleaseNotes.md | 7 +
clang/lib/AST/ASTContext.cpp | 6 +-
.../SemaTemplate/temp_arg_nontype_cxx1z.cpp | 162 ++++++++++++++++--
3 files changed, 160 insertions(+), 15 deletions(-)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index a1f24a8caedae..63e698a84a53b 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -727,6 +727,13 @@ features cannot lower the translation-unit ABI level;
- Fixed ambiguous overload where two non-static member functions with
different signatures could be incorrectly considered equivalent. (#GH224499)
+- Fixed template argument deduction incorrectly selecting a class or variable
+ template partial specialization whose non-type template argument has a
+ different type from the argument it is matched against, when the corresponding
+ parameter of the primary template has a placeholder type. For example, given
+ `template <class T, auto V> struct S`, the partial specialization `S<T, 0>` is
+ no longer selected for `S<void, 0L>`. (#GH124186), (#GH42421), (#GH53982)
+
#### Bug Fixes to AST Handling
- Fixed a non-deterministic ordering of unused local typedefs that made
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index ee8663e6906af..926a8f7b7ee4b 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -8040,7 +8040,11 @@ bool ASTContext::isSameTemplateArgument(const TemplateArgument &Arg1,
getCanonicalTemplateName(Arg2.getAsTemplateOrTemplatePattern());
case TemplateArgument::Integral:
- return llvm::APSInt::isSameValue(Arg1.getAsIntegral(),
+ // The types have to match as well as the values:
+ // C++ [temp.type]p2
+ // C++ [temp.deduct.type]p20
+ return hasSameType(Arg1.getIntegralType(), Arg2.getIntegralType()) &&
+ llvm::APSInt::isSameValue(Arg1.getAsIntegral(),
Arg2.getAsIntegral());
case TemplateArgument::StructuralValue:
diff --git a/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp b/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp
index a39bb02084aa7..1621983be64fa 100644
--- a/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp
+++ b/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp
@@ -267,22 +267,23 @@ namespace Auto {
}
namespace Decomposition {
- // Types of deduced non-type template arguments must match exactly, so
- // partial ordering fails in both directions here.
- template<auto> struct Any;
- template<int N> struct Any<N> { typedef int Int; }; // expected-note 3{{match}}
- template<short N> struct Any<N> { typedef int Short; }; // expected-note 3{{match}}
- Any<0>::Int is_int; // expected-error {{ambiguous}}
- Any<(short)0>::Short is_short; // expected-error {{ambiguous}}
- Any<(char)0>::Short is_char; // expected-error {{ambiguous}}
+ // Types of deduced non-type template arguments must match exactly, so each
+ // of these selects at most one partial specialization: the one whose
+ // parameter has the same type as the argument.
+ template<auto> struct Any; // expected-note {{template is declared here}}
+ template<int N> struct Any<N> { typedef int Int; };
+ template<short N> struct Any<N> { typedef int Short; };
+ Any<0>::Int is_int;
+ Any<(short)0>::Short is_short;
+ Any<(char)0>::Short is_char; // expected-error {{implicit instantiation of undefined template 'Auto::Decomposition::Any<'\x00'>'}}
template<int, auto> struct NestedAny;
- template<auto N> struct NestedAny<0, N>; // expected-note 3{{match}}
- template<int N> struct NestedAny<0, N> { typedef int Int; }; // expected-note 3{{match}}
- template<short N> struct NestedAny<0, N> { typedef int Short; }; // expected-note 3{{match}}
- NestedAny<0, 0>::Int nested_int; // expected-error {{ambiguous}}
- NestedAny<0, (short)0>::Short nested_short; // expected-error {{ambiguous}}
- NestedAny<0, (char)0>::Short nested_char; // expected-error {{ambiguous}}
+ template<auto N> struct NestedAny<0, N>; // expected-note {{template is declared here}}
+ template<int N> struct NestedAny<0, N> { typedef int Int; };
+ template<short N> struct NestedAny<0, N> { typedef int Short; };
+ NestedAny<0, 0>::Int nested_int;
+ NestedAny<0, (short)0>::Short nested_short;
+ NestedAny<0, (char)0>::Short nested_char; // expected-error {{implicit instantiation of undefined template 'Auto::Decomposition::NestedAny<0, '\x00'>'}}
double foo(int, bool);
template<auto& f> struct fn_result_type;
@@ -649,6 +650,139 @@ namespace GH58682 {
static_assert(B<A<(g)>>::k == 1, "");
} // namespace GH58682
+namespace GH124186 {
+ template <class T, auto V> struct S {
+ static constexpr int value = 0;
+ };
+
+ template <class T> struct S<T, 0> {
+ static constexpr int value = 1;
+ };
+
+ enum E { Zero };
+
+ static_assert(S<void, 0>::value == 1);
+ static_assert(S<void, 0L>::value == 0);
+ static_assert(S<void, 0U>::value == 0);
+ static_assert(S<void, false>::value == 0);
+ static_assert(S<void, Zero>::value == 0);
+
+ // For a parameter of non-placeholder type the argument is converted to the
+ // type of the parameter, so these all still match.
+ template <class T, int V> struct F {
+ static constexpr int value = 0;
+ };
+
+ template <class T> struct F<T, 0> {
+ static constexpr int value = 1;
+ };
+
+ static_assert(F<void, 0>::value == 1);
+ static_assert(F<void, 0L>::value == 1);
+ static_assert(F<void, 0U>::value == 1);
+
+ // Test with aliasing and cv modifiers
+ typedef int my_int;
+ using my_const_int = const int;
+
+ template <class T, auto V> struct G {
+ static constexpr int value = 0;
+ };
+
+ template <class T> struct G<T, (my_int)0> {
+ static constexpr int value = 1;
+ };
+
+ static_assert(G<void, 0>::value == 1);
+ static_assert(G<void, 0L>::value == 0);
+
+ template <auto V> struct H {
+ static constexpr int value = 0;
+ };
+
+ template <my_int V> struct H<V> {
+ static constexpr int value = 1;
+ };
+
+ template <auto V> struct I {
+ static constexpr int value = 0;
+ };
+
+ template <my_const_int V> struct I<V> {
+ static constexpr int value = 1;
+ };
+
+ static_assert(H<0>::value == 1);
+ static_assert(H<0L>::value == 0);
+ static_assert(I<0>::value == 1);
+ static_assert(I<0L>::value == 0);
+
+ // Variable templates behave the same way.
+ template <class T, auto V> constexpr int value = 0;
+ template <class T> constexpr int value<T, 0> = 1;
+
+ static_assert(value<void, 0> == 1);
+ static_assert(value<void, 0L> == 0);
+ static_assert(value<void, 0U> == 0);
+ static_assert(value<void, false> == 0);
+} // namespace GH124186
+
+namespace GH42421 {
+ template <auto V> struct S {
+ static constexpr int value = 0;
+ };
+
+ template <int I> struct S<I> {
+ static constexpr int value = 1;
+ };
+
+ static_assert(S<42>::value == 1);
+ // A long or unsigned argument does not match an int parameter.
+ static_assert(S<42L>::value == 0);
+ static_assert(S<42U>::value == 0);
+
+ // Only the partial specialization is type-sensitive here; the explicit
+ // specialization is not a candidate for Q<42U, int> either way.
+ template <auto I, class T> struct Q {
+ static constexpr int value = 0;
+ };
+
+ template <class T> struct Q<42, T> {
+ static constexpr int value = 1;
+ };
+
+ template <> struct Q<42, int> {
+ static constexpr int value = 2;
+ };
+
+ static_assert(Q<42U, int>::value == 0);
+ // The explicit specialization still wins for an int argument.
+ static_assert(Q<42, int>::value == 2);
+} // namespace GH42421
+
+namespace GH53982 {
+ enum class E1 : unsigned int { E11 = 1 };
+ enum class E2 : unsigned int { E21 = 1 };
+
+ template <int j, auto i> struct C {
+ static constexpr int value = 0;
+ };
+
+ template <int j> struct C<j, E1::E11> {
+ static constexpr int value = 1;
+ };
+
+ template <int j> struct C<j, E2::E21> {
+ static constexpr int value = 2;
+ };
+
+ static_assert(C<0, E1::E11>::value == 1);
+ static_assert(C<1, E2::E21>::value == 2);
+
+ // The shared underlying value on its own matches neither.
+ static_assert(C<0, 1U>::value == 0);
+} // namespace GH53982
+
// C++26 [temp.deduct.type]p13, Example 8.
namespace temp_deduct_type_p13 {
template<long n> struct A { };
>From c39fe2aaef36210dfe226e5105f016e541e11024 Mon Sep 17 00:00:00 2001
From: Valentyn Yukhymenko <valentin.yukhymenko at gmail.com>
Date: Tue, 22 Sep 2026 01:37:28 +0100
Subject: [PATCH 2/4] Simplify error message matching
---
clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp b/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp
index 1621983be64fa..1472aaa632400 100644
--- a/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp
+++ b/clang/test/SemaTemplate/temp_arg_nontype_cxx1z.cpp
@@ -275,7 +275,7 @@ namespace Auto {
template<short N> struct Any<N> { typedef int Short; };
Any<0>::Int is_int;
Any<(short)0>::Short is_short;
- Any<(char)0>::Short is_char; // expected-error {{implicit instantiation of undefined template 'Auto::Decomposition::Any<'\x00'>'}}
+ Any<(char)0>::Short is_char; // expected-error {{implicit instantiation of undefined template}}
template<int, auto> struct NestedAny;
template<auto N> struct NestedAny<0, N>; // expected-note {{template is declared here}}
@@ -283,7 +283,7 @@ namespace Auto {
template<short N> struct NestedAny<0, N> { typedef int Short; };
NestedAny<0, 0>::Int nested_int;
NestedAny<0, (short)0>::Short nested_short;
- NestedAny<0, (char)0>::Short nested_char; // expected-error {{implicit instantiation of undefined template 'Auto::Decomposition::NestedAny<0, '\x00'>'}}
+ NestedAny<0, (char)0>::Short nested_char; // expected-error {{implicit instantiation of undefined template}}
double foo(int, bool);
template<auto& f> struct fn_result_type;
>From ecba51ddc3bea0baa9b9ca4a1d62972456efc826 Mon Sep 17 00:00:00 2001
From: Valentyn Yukhymenko <valentin.yukhymenko at gmail.com>
Date: Tue, 22 Sep 2026 01:39:16 +0100
Subject: [PATCH 3/4] Remove second reference to temp.deduce
---
clang/lib/AST/ASTContext.cpp | 1 -
1 file changed, 1 deletion(-)
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index ef078a12ba164..799f2bfc48a80 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -8042,7 +8042,6 @@ bool ASTContext::isSameTemplateArgument(const TemplateArgument &Arg1,
case TemplateArgument::Integral:
// The types have to match as well as the values:
// C++ [temp.type]p2
- // C++ [temp.deduct.type]p20
return hasSameType(Arg1.getIntegralType(), Arg2.getIntegralType()) &&
llvm::APSInt::isSameValue(Arg1.getAsIntegral(),
Arg2.getAsIntegral());
>From 4e6ef2d94490ff84b9f4f45a07898472a219b62d Mon Sep 17 00:00:00 2001
From: BaLiKfromUA <valentin.yukhymenko at gmail.com>
Date: Wed, 23 Sep 2026 22:56:07 +0100
Subject: [PATCH 4/4] Add more precise reference to wording
---
clang/lib/AST/ASTContext.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index 799f2bfc48a80..c6087cf4ba98d 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -8040,8 +8040,9 @@ bool ASTContext::isSameTemplateArgument(const TemplateArgument &Arg1,
getCanonicalTemplateName(Arg2.getAsTemplateOrTemplatePattern());
case TemplateArgument::Integral:
- // The types have to match as well as the values:
- // C++ [temp.type]p2
+ // Per C++20 [temp.type]p2:
+ // "Two values are template-argument-equivalent if they are of the same type
+ // and they are of integral type and their values are the same".
return hasSameType(Arg1.getIntegralType(), Arg2.getIntegralType()) &&
llvm::APSInt::isSameValue(Arg1.getAsIntegral(),
Arg2.getAsIntegral());
More information about the cfe-commits
mailing list