Skip to content

[Clang] Fix reference binding traits with cv-qualified array types#198698

Open
frederick-vs-ja wants to merge 1 commit into
llvm:mainfrom
frederick-vs-ja:clang-lwg-3819-array
Open

[Clang] Fix reference binding traits with cv-qualified array types#198698
frederick-vs-ja wants to merge 1 commit into
llvm:mainfrom
frederick-vs-ja:clang-lwg-3819-array

Conversation

@frederick-vs-ja

@frederick-vs-ja frederick-vs-ja commented May 20, 2026

Copy link
Copy Markdown
Contributor

When the source type is a cv-qualified array type that is denoted by an alias, Clang currently incorrectly drops cv-qualifiers from the array type.

Per C++26 [expr.type]p2 (initially fixed by CWG1261), cv-qualification is kept for array prvalues.

HLSL is consistently affected - this patch makes __decltype keep address space qualifiers for array prvalues in HLSL.

Fixes #198580.

@llvmorg-github-actions llvmorg-github-actions Bot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels May 20, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-hlsl

@llvm/pr-subscribers-clang

Author: A. Jiang (frederick-vs-ja)

Changes

When the source type is a cv-qualified array type that is denoted by an alias, Clang currently incorrectly drops cv-qualifiers from the array type.

Per C++26 [expr.type]p2 (initially fixed by CWG1261), cv-qualification is kept for array prvalues.

Fixes #198580.


Full diff: https://github.com/llvm/llvm-project/pull/198698.diff

3 Files Affected:

  • (modified) clang/docs/ReleaseNotes.rst (+2)
  • (modified) clang/lib/AST/Type.cpp (+6-4)
  • (modified) clang/test/SemaCXX/type-traits.cpp (+76)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 843a01f57c39f..afb10d4340599 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -570,6 +570,8 @@ Bug Fixes in This Version
   an array via an element-at-a-time copy loop (#GH192026)
 - Fixed an issue where certain designated initializers would be rejected for constexpr variables. (#GH193373)
 - Fixed a crash when ``#embed`` is used with C++ modules (#GH195350)
+- Fixed mishandling of cv-qualified array types in ``__reference_constructs_from_temporary`` and
+  ``__reference_converts_from_temporary`` builtins. (#GH198580)
 
 Bug Fixes to Compiler Builtins
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/clang/lib/AST/Type.cpp b/clang/lib/AST/Type.cpp
index 96a398aa21dad..cda3d8dba08e2 100644
--- a/clang/lib/AST/Type.cpp
+++ b/clang/lib/AST/Type.cpp
@@ -3679,13 +3679,15 @@ QualType QualType::getNonLValueExprType(const ASTContext &Context) const {
   if (const auto *RefType = getTypePtr()->getAs<ReferenceType>())
     return RefType->getPointeeType();
 
-  // C++0x [basic.lval]:
-  //   Class prvalues can have cv-qualified types; non-class prvalues always
-  //   have cv-unqualified types.
+  // C++26 [expr.type]p2 (see also CWG1261):
+  //   If a prvalue initially has the type "cv T", where T is a cv-unqualified
+  //   non-class, non-array type, the type of the expression is adjusted to T
+  //   prior to any further analysis.
   //
   // See also C99 6.3.2.1p2.
   if (!Context.getLangOpts().CPlusPlus ||
-      (!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType()))
+      (!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType() &&
+       !getTypePtr()->isArrayType()))
     return getUnqualifiedType();
 
   return *this;
diff --git a/clang/test/SemaCXX/type-traits.cpp b/clang/test/SemaCXX/type-traits.cpp
index 8decb1f61395e..3b2fe8a78f37e 100644
--- a/clang/test/SemaCXX/type-traits.cpp
+++ b/clang/test/SemaCXX/type-traits.cpp
@@ -3310,6 +3310,44 @@ void reference_constructs_from_temporary_checks() {
   // For scalar types, cv-qualifications are dropped first for prvalues.
   static_assert(__reference_constructs_from_temporary(int&&, const int));
   static_assert(__reference_constructs_from_temporary(int&&, volatile int));
+  // For array types, cv-qualifications are *kept* for prvalues.
+  // Uses with type aliases need to be verified, see https://llvm.org/PR198580.
+  static_assert(!__reference_constructs_from_temporary(const int(&)[10], volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(const IntAr&, volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(const int(&)[10], volatile IntAr));
+  static_assert(!__reference_constructs_from_temporary(const IntAr&, volatile IntAr));
+  static_assert(!__reference_constructs_from_temporary(int(&&)[10], volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(IntAr&&, volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(int(&&)[10], volatile IntAr));
+  static_assert(!__reference_constructs_from_temporary(IntAr&&, volatile IntAr));
+  static_assert(__reference_constructs_from_temporary(volatile int(&&)[10], volatile int[10]));
+  static_assert(__reference_constructs_from_temporary(volatile IntAr&&, volatile int[10]));
+  static_assert(__reference_constructs_from_temporary(volatile int(&&)[10], volatile IntAr));
+  static_assert(__reference_constructs_from_temporary(volatile IntAr&&, volatile IntAr));
+  static_assert(!__reference_constructs_from_temporary(int*&&, volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(int*&&, volatile IntAr));
+  static_assert(!__reference_constructs_from_temporary(int* const&, volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(int* const&, volatile IntAr));
+  static_assert(__reference_constructs_from_temporary(volatile int*&&, volatile int[10]));
+  static_assert(__reference_constructs_from_temporary(volatile int*&&, volatile IntAr));
+  static_assert(!__reference_constructs_from_temporary(const Empty(&)[10], volatile Empty[10]));
+  static_assert(!__reference_constructs_from_temporary(const EmptyAr&, volatile Empty[10]));
+  static_assert(!__reference_constructs_from_temporary(const Empty(&)[10], volatile Empty));
+  static_assert(!__reference_constructs_from_temporary(const EmptyAr&, volatile Empty));
+  static_assert(!__reference_constructs_from_temporary(Empty(&&)[10], volatile Empty[10]));
+  static_assert(!__reference_constructs_from_temporary(EmptyAr&&, volatile int[10]));
+  static_assert(!__reference_constructs_from_temporary(Empty(&&)[10], volatile EmptyAr));
+  static_assert(!__reference_constructs_from_temporary(EmptyAr&&, volatile EmptyAr));
+  static_assert(__reference_constructs_from_temporary(volatile Empty(&&)[10], volatile Empty[10]));
+  static_assert(__reference_constructs_from_temporary(volatile EmptyAr&&, volatile Empty[10]));
+  static_assert(__reference_constructs_from_temporary(volatile Empty(&&)[10], volatile EmptyAr));
+  static_assert(__reference_constructs_from_temporary(volatile EmptyAr&&, volatile EmptyAr));
+  static_assert(!__reference_constructs_from_temporary(Empty*&&, volatile Empty[10]));
+  static_assert(!__reference_constructs_from_temporary(Empty*&&, volatile EmptyAr));
+  static_assert(!__reference_constructs_from_temporary(Empty* const&, volatile Empty[10]));
+  static_assert(!__reference_constructs_from_temporary(Empty* const&, volatile EmptyAr));
+  static_assert(__reference_constructs_from_temporary(volatile Empty*&&, volatile Empty[10]));
+  static_assert(__reference_constructs_from_temporary(volatile Empty*&&, volatile EmptyAr));
 
   // Additional checks
   static_assert(__reference_constructs_from_temporary(POD const&, Derives));
@@ -3377,6 +3415,44 @@ void reference_converts_from_temporary_checks() {
   // For scalar types, cv-qualifications are dropped first for prvalues.
   static_assert(__reference_converts_from_temporary(int&&, const int));
   static_assert(__reference_converts_from_temporary(int&&, volatile int));
+  // For array types, cv-qualifications are *kept* for prvalues.
+  // Uses with type aliases need to be verified, see https://llvm.org/PR198580.
+  static_assert(!__reference_converts_from_temporary(const int(&)[10], volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(const IntAr&, volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(const int(&)[10], volatile IntAr));
+  static_assert(!__reference_converts_from_temporary(const IntAr&, volatile IntAr));
+  static_assert(!__reference_converts_from_temporary(int(&&)[10], volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(IntAr&&, volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(int(&&)[10], volatile IntAr));
+  static_assert(!__reference_converts_from_temporary(IntAr&&, volatile IntAr));
+  static_assert(__reference_converts_from_temporary(volatile int(&&)[10], volatile int[10]));
+  static_assert(__reference_converts_from_temporary(volatile IntAr&&, volatile int[10]));
+  static_assert(__reference_converts_from_temporary(volatile int(&&)[10], volatile IntAr));
+  static_assert(__reference_converts_from_temporary(volatile IntAr&&, volatile IntAr));
+  static_assert(!__reference_converts_from_temporary(int*&&, volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(int*&&, volatile IntAr));
+  static_assert(!__reference_converts_from_temporary(int* const&, volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(int* const&, volatile IntAr));
+  static_assert(__reference_converts_from_temporary(volatile int*&&, volatile int[10]));
+  static_assert(__reference_converts_from_temporary(volatile int*&&, volatile IntAr));
+  static_assert(!__reference_converts_from_temporary(const Empty(&)[10], volatile Empty[10]));
+  static_assert(!__reference_converts_from_temporary(const EmptyAr&, volatile Empty[10]));
+  static_assert(!__reference_converts_from_temporary(const Empty(&)[10], volatile Empty));
+  static_assert(!__reference_converts_from_temporary(const EmptyAr&, volatile Empty));
+  static_assert(!__reference_converts_from_temporary(Empty(&&)[10], volatile Empty[10]));
+  static_assert(!__reference_converts_from_temporary(EmptyAr&&, volatile int[10]));
+  static_assert(!__reference_converts_from_temporary(Empty(&&)[10], volatile EmptyAr));
+  static_assert(!__reference_converts_from_temporary(EmptyAr&&, volatile EmptyAr));
+  static_assert(__reference_converts_from_temporary(volatile Empty(&&)[10], volatile Empty[10]));
+  static_assert(__reference_converts_from_temporary(volatile EmptyAr&&, volatile Empty[10]));
+  static_assert(__reference_converts_from_temporary(volatile Empty(&&)[10], volatile EmptyAr));
+  static_assert(__reference_converts_from_temporary(volatile EmptyAr&&, volatile EmptyAr));
+  static_assert(!__reference_converts_from_temporary(Empty*&&, volatile Empty[10]));
+  static_assert(!__reference_converts_from_temporary(Empty*&&, volatile EmptyAr));
+  static_assert(!__reference_converts_from_temporary(Empty* const&, volatile Empty[10]));
+  static_assert(!__reference_converts_from_temporary(Empty* const&, volatile EmptyAr));
+  static_assert(__reference_converts_from_temporary(volatile Empty*&&, volatile Empty[10]));
+  static_assert(__reference_converts_from_temporary(volatile Empty*&&, volatile EmptyAr));
 
   // Additional checks
   static_assert(__reference_converts_from_temporary(POD const&, Derives));

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 117981 tests passed
  • 4738 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 55610 tests passed
  • 2578 tests skipped

✅ The build succeeded and all tests passed.

Comment thread clang/lib/AST/Type.cpp
if (!Context.getLangOpts().CPlusPlus ||
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType()))
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType() &&
!getTypePtr()->isArrayType()))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test failure was related to this.

template<typename T>
struct remove_addrspace {
using type = __decltype((T)0);
};

After the seemingly reasonable change in QualType::getNonPackExpansionType, __decltype((T)0) fails to remove hlsl_private from an array type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure can be resolved by __typeof_unqual__. However, there's still potential breaking change of __decltype for HLSL. Should we note this in release note or avoid behavior change for HLSL?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CC @llvm-beanz because it's an HLSL question I don't feel confident answering.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a non-lvalue expression type, won't the array have been decayed to a pointer already?

When the source type is a cv-qualified array type that is denoted by an
alias, Clang currently incorrectly drops cv-qualifiers from the array
type.

Per C++26 [expr.type]p2 (initially fixed by CWG1261), cv-qualification
is kept for array prvalues.

HLSL is consistently affected - this patch makes `__decltype` keep
address space qualifiers for array prvalues in HLSL.

@cor3ntin cor3ntin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be willing to ad a dr test for cwg1261 in this PR?

@frederick-vs-ja

Copy link
Copy Markdown
Contributor Author

Pre-CWG1261 wording is also quoted in some other location(s) now. It's possible that Clang has already effectively implemented CWG1261, but it's a bit too tiring for me to check the implementation status thoroughly at this moment.

@Endilll Endilll left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is backported to all language modes without restrictions. Do we need an ABI tag?
CC @AaronBallman

Comment thread clang/lib/AST/Type.cpp
if (!Context.getLangOpts().CPlusPlus ||
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType()))
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType() &&
!getTypePtr()->isArrayType()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Between this code and [basic.type]/2, this change seems correct. However, this change seems to be placed rather deep compared to the goal of fixing two type traits. Could it be that we have duplicated code paths in SemaInit.cpp that should be calling this function?

@AaronBallman AaronBallman requested a review from llvm-beanz May 22, 2026 15:23
Comment thread clang/lib/AST/Type.cpp
if (!Context.getLangOpts().CPlusPlus ||
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType()))
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType() &&
!getTypePtr()->isArrayType()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a non-lvalue expression type, won't the array have been decayed to a pointer already?

Comment thread clang/lib/AST/Type.cpp
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType()))
(!getTypePtr()->isDependentType() && !getTypePtr()->isRecordType() &&
!getTypePtr()->isArrayType()))
return getUnqualifiedType();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by: are we sure this is correct for _Atomic in C? (This relates to the question of whether we expect this to be called after rvalue conversion or not; if the rvalue conversion has happened before calling this, then the type won't be atomic any longer anyway.)

@AaronBallman

Copy link
Copy Markdown
Contributor

This change is backported to all language modes without restrictions. Do we need an ABI tag? CC @AaronBallman

Because it impacts type traits, I think an ABI tag would be warranted (every time we change type traits, we usually quietly break how some template specializations get resolved).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category HLSL HLSL Language Support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Clang] reference_constructs_from_temporary mishandles cv-qualified arrays

5 participants