[clang] [clang] Implement CWG3135 - constexpr structured bindings with prvalues from tuples (PR #191880)
Matthias Wippich via cfe-commits
cfe-commits at lists.llvm.org
Sat Apr 18 19:18:20 PDT 2026
https://github.com/Tsche updated https://github.com/llvm/llvm-project/pull/191880
>From b518335e831b1d1fbd147f111adfe575f1e4d418 Mon Sep 17 00:00:00 2001
From: Matthias Wippich <mfwippich at gmail.com>
Date: Mon, 13 Apr 2026 21:38:34 +0200
Subject: [PATCH 1/4] [clang] implement CWG3135 - constexpr structured bindings
with prvalues from tuples
---
clang/docs/ReleaseNotes.rst | 3 ++
clang/lib/Sema/SemaDeclCXX.cpp | 30 +++++++++------
...egen-for-constexpr-structured-bindings.cpp | 2 +-
.../SemaCXX/cxx2c-decomposition-prvalues.cpp | 37 +++++++++++++++++++
clang/test/SemaCXX/cxx2c-decomposition.cpp | 4 +-
5 files changed, 61 insertions(+), 15 deletions(-)
create mode 100644 clang/test/SemaCXX/cxx2c-decomposition-prvalues.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index fd58d7847717c..63c27d1fb7066 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -49,6 +49,9 @@ C++ Specific Potentially Breaking Changes
- Clang now correctly rejects ``export`` declarations in module implementation
partitions. (#GH107602)
+- Clang now uses non-reference types for structured bindings whose initializer
+ returns a prvalue.
+
ABI Changes in This Version
---------------------------
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 6647e52535114..e5855196da504 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -1361,25 +1361,28 @@ static bool checkTupleLikeDecomposition(Sema &S,
return true;
Expr *Init = E.get();
- // Given the type T designated by std::tuple_element<i - 1, E>::type,
+ // Given the type T designated by std::tuple_element<i - 1, E>::type
QualType T = getTupleLikeElementType(S, Loc, I, DecompType);
if (T.isNull())
return true;
- // each vi is a variable of type "reference to T" initialized with the
- // initializer, where the reference is an lvalue reference if the
- // initializer is an lvalue and an rvalue reference otherwise
- QualType RefType =
- S.BuildReferenceType(T, E.get()->isLValue(), Loc, B->getDeclName());
- if (RefType.isNull())
+ // C++26 [dcl.struct.bind]p7:
+ // and the type Ui, defined as Ti if the initializer is a prvalue,
+ // as "lvalue reference to Ti" if the initializer is an lvalue,
+ // or as "rvalue reference to Ti" otherwise
+ // "defined as Ti if the initializer is a prvalue" was introduced by CWG3135
+ QualType U = E.get()->isPRValue() && S.getLangOpts().CPlusPlus26
+ ? T
+ : S.BuildReferenceType(T, E.get()->isLValue(), Loc,
+ B->getDeclName());
+ if (U.isNull())
return true;
// Don't give this VarDecl a TypeSourceInfo, since this is a synthesized
// entity and this type was never written in source code.
- auto *RefVD =
- VarDecl::Create(S.Context, Src->getDeclContext(), Loc, Loc,
- B->getDeclName().getAsIdentifierInfo(), RefType,
- /*TInfo=*/nullptr, Src->getStorageClass());
+ auto *RefVD = VarDecl::Create(S.Context, Src->getDeclContext(), Loc, Loc,
+ B->getDeclName().getAsIdentifierInfo(), U,
+ /*TInfo=*/nullptr, Src->getStorageClass());
RefVD->setLexicalDeclContext(Src->getLexicalDeclContext());
RefVD->setTSCSpec(Src->getTSCSpec());
RefVD->setImplicit();
@@ -1388,7 +1391,10 @@ static bool checkTupleLikeDecomposition(Sema &S,
RefVD->getLexicalDeclContext()->addHiddenDecl(RefVD);
InitializedEntity Entity = InitializedEntity::InitializeBinding(RefVD);
- InitializationKind Kind = InitializationKind::CreateCopy(Loc, Loc);
+ InitializationKind Kind =
+ E.get()->isPRValue() && S.getLangOpts().CPlusPlus26
+ ? InitializationKind::CreateDirect(Loc, Loc, Loc)
+ : InitializationKind::CreateCopy(Loc, Loc);
InitializationSequence Seq(S, Entity, Kind, Init);
E = Seq.Perform(S, Entity, Kind, Init);
if (E.isInvalid())
diff --git a/clang/test/CodeGenCXX/bad-codegen-for-constexpr-structured-bindings.cpp b/clang/test/CodeGenCXX/bad-codegen-for-constexpr-structured-bindings.cpp
index ce81f7d8d026e..87c2055627aa7 100644
--- a/clang/test/CodeGenCXX/bad-codegen-for-constexpr-structured-bindings.cpp
+++ b/clang/test/CodeGenCXX/bad-codegen-for-constexpr-structured-bindings.cpp
@@ -35,6 +35,6 @@ const u8 &f() {
return I;
}
-// CHECK: @[[TMP:_ZGR.*]] = internal constant i8 0, align 1
+// CHECK: @[[TMP:_ZZ1fvE1I]] = internal constant i8 0, align 1
// CHECK-LABEL: define {{.*}} @_Z1fv(
// CHECK: ret ptr @[[TMP]]
diff --git a/clang/test/SemaCXX/cxx2c-decomposition-prvalues.cpp b/clang/test/SemaCXX/cxx2c-decomposition-prvalues.cpp
new file mode 100644
index 0000000000000..e7a1a17c3168f
--- /dev/null
+++ b/clang/test/SemaCXX/cxx2c-decomposition-prvalues.cpp
@@ -0,0 +1,37 @@
+// RUN: %clang_cc1 -std=c++23 -fsyntax-only -verify %s
+// expected-no-diagnostics
+// RUN: %clang_cc1 -std=c++2c -fsyntax-only -verify=since-cxx26 %s
+
+
+namespace std {
+ using size_t = decltype(sizeof(0));
+ template<typename> struct tuple_size;
+ template<size_t, typename> struct tuple_element;
+}
+
+struct Pinned {
+ Pinned(const Pinned&) = delete;
+ // since-cxx26-note at -1 {{'Pinned' has been explicitly marked deleted here}}
+ Pinned& operator=(const Pinned&) = delete;
+};
+
+struct Source {
+ operator Pinned&&() const;
+
+ template<std::size_t>
+ Source get() noexcept;
+};
+
+template<>
+struct std::tuple_size<Source> {
+ static constexpr std::size_t value = 1;
+};
+
+template<>
+struct std::tuple_element<0, Source> { using type = Pinned; };
+
+// CWG3135: In C++26 mode `x` is of type Pinned rather than Pinned&&.
+// This leads to the deleted copy ctor being called in C++26 mode.
+auto [x] = Source{};
+// since-cxx26-error-re at -1 {{call to deleted constructor of 'std::tuple_element<0U{{L*}}, Source>::type' (aka 'Pinned')}}
+// since-cxx26-note at -2 {{in implicit initialization of binding declaration 'x'}}
\ No newline at end of file
diff --git a/clang/test/SemaCXX/cxx2c-decomposition.cpp b/clang/test/SemaCXX/cxx2c-decomposition.cpp
index 99278c6575ef1..df2e3fa90263a 100644
--- a/clang/test/SemaCXX/cxx2c-decomposition.cpp
+++ b/clang/test/SemaCXX/cxx2c-decomposition.cpp
@@ -66,8 +66,8 @@ void test() {
constexpr auto [a, b] = B{};
static_assert(a.n == 0);
// expected-error at -1 {{static assertion expression is not an integral constant expression}} \
-// expected-note at -1 {{read of temporary is not allowed in a constant expression outside the expression that created the temporary}}\
-// expected-note at -2 {{temporary created here}}
+// expected-note at -1 {{read of non-constexpr variable 'a' is not allowed in a constant expression}} \
+// expected-note at -2 {{declared here}}
constinit auto [init1] = Y {42};
constinit auto [init2] = X {}; // expected-error {{variable does not have a constant initializer}} \
>From 762b8a088416b2215cdcf8c604632f77b7117e98 Mon Sep 17 00:00:00 2001
From: Matthias Wippich <mfwippich at gmail.com>
Date: Sun, 19 Apr 2026 02:52:28 +0200
Subject: [PATCH 2/4] move test
---
.../cxx2c-decomposition-prvalues.cpp => CXX/drs/cwg3135.cpp} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename clang/test/{SemaCXX/cxx2c-decomposition-prvalues.cpp => CXX/drs/cwg3135.cpp} (100%)
diff --git a/clang/test/SemaCXX/cxx2c-decomposition-prvalues.cpp b/clang/test/CXX/drs/cwg3135.cpp
similarity index 100%
rename from clang/test/SemaCXX/cxx2c-decomposition-prvalues.cpp
rename to clang/test/CXX/drs/cwg3135.cpp
>From ab46e65f214a3294562db5dc090d0bba01a24603 Mon Sep 17 00:00:00 2001
From: Matthias Wippich <mfwippich at gmail.com>
Date: Sun, 19 Apr 2026 03:04:25 +0200
Subject: [PATCH 3/4] treat CWG3135 as DR, resolve review comments
---
clang/docs/ReleaseNotes.rst | 6 +++---
clang/lib/Sema/SemaDeclCXX.cpp | 7 +++----
clang/test/CXX/drs/cwg3135.cpp | 11 +++++------
3 files changed, 11 insertions(+), 13 deletions(-)
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 63c27d1fb7066..27e2649c01b55 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -49,9 +49,6 @@ C++ Specific Potentially Breaking Changes
- Clang now correctly rejects ``export`` declarations in module implementation
partitions. (#GH107602)
-- Clang now uses non-reference types for structured bindings whose initializer
- returns a prvalue.
-
ABI Changes in This Version
---------------------------
@@ -144,6 +141,9 @@ C++17 Feature Support
Resolutions to C++ Defect Reports
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+- Clang now uses non-reference types for structured bindings whose initializer
+ returns a prvalue. This resolves CWG3135.
+
C Language Changes
------------------
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index e5855196da504..da92b0dfac056 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -1371,7 +1371,7 @@ static bool checkTupleLikeDecomposition(Sema &S,
// as "lvalue reference to Ti" if the initializer is an lvalue,
// or as "rvalue reference to Ti" otherwise
// "defined as Ti if the initializer is a prvalue" was introduced by CWG3135
- QualType U = E.get()->isPRValue() && S.getLangOpts().CPlusPlus26
+ QualType U = E.get()->isPRValue()
? T
: S.BuildReferenceType(T, E.get()->isLValue(), Loc,
B->getDeclName());
@@ -1392,9 +1392,8 @@ static bool checkTupleLikeDecomposition(Sema &S,
InitializedEntity Entity = InitializedEntity::InitializeBinding(RefVD);
InitializationKind Kind =
- E.get()->isPRValue() && S.getLangOpts().CPlusPlus26
- ? InitializationKind::CreateDirect(Loc, Loc, Loc)
- : InitializationKind::CreateCopy(Loc, Loc);
+ E.get()->isPRValue() ? InitializationKind::CreateDirect(Loc, Loc, Loc)
+ : InitializationKind::CreateCopy(Loc, Loc);
InitializationSequence Seq(S, Entity, Kind, Init);
E = Seq.Perform(S, Entity, Kind, Init);
if (E.isInvalid())
diff --git a/clang/test/CXX/drs/cwg3135.cpp b/clang/test/CXX/drs/cwg3135.cpp
index e7a1a17c3168f..abd7986dffec1 100644
--- a/clang/test/CXX/drs/cwg3135.cpp
+++ b/clang/test/CXX/drs/cwg3135.cpp
@@ -1,6 +1,5 @@
-// RUN: %clang_cc1 -std=c++23 -fsyntax-only -verify %s
-// expected-no-diagnostics
-// RUN: %clang_cc1 -std=c++2c -fsyntax-only -verify=since-cxx26 %s
+// RUN: %clang_cc1 -std=c++2c -fsyntax-only -verify %s
+// RUN: %clang_cc1 -fexperimental-new-constant-interpreter -std=c++2c -fsyntax-only -verify %s
namespace std {
@@ -11,7 +10,7 @@ namespace std {
struct Pinned {
Pinned(const Pinned&) = delete;
- // since-cxx26-note at -1 {{'Pinned' has been explicitly marked deleted here}}
+ // expected-note at -1 {{'Pinned' has been explicitly marked deleted here}}
Pinned& operator=(const Pinned&) = delete;
};
@@ -33,5 +32,5 @@ struct std::tuple_element<0, Source> { using type = Pinned; };
// CWG3135: In C++26 mode `x` is of type Pinned rather than Pinned&&.
// This leads to the deleted copy ctor being called in C++26 mode.
auto [x] = Source{};
-// since-cxx26-error-re at -1 {{call to deleted constructor of 'std::tuple_element<0U{{L*}}, Source>::type' (aka 'Pinned')}}
-// since-cxx26-note at -2 {{in implicit initialization of binding declaration 'x'}}
\ No newline at end of file
+// expected-error-re at -1 {{call to deleted constructor of 'std::tuple_element<0U{{L*}}, Source>::type' (aka 'Pinned')}}
+// expected-note at -2 {{in implicit initialization of binding declaration 'x'}}
>From ee914556bbe03e9f900d4432d2416487b54f9558 Mon Sep 17 00:00:00 2001
From: Matthias Wippich <mfwippich at gmail.com>
Date: Sun, 19 Apr 2026 04:11:49 +0200
Subject: [PATCH 4/4] fix failing tests, remove tests that no longer make sense
---
clang/test/Analysis/anonymous-decls.cpp | 14 ++++++--------
clang/test/Analysis/live-bindings-test.cpp | 11 -----------
clang/test/CXX/dcl.decl/dcl.decomp/p3.cpp | 3 ---
clang/test/CodeGenCXX/cxx1z-decomposition.cpp | 13 +++++--------
clang/test/DebugInfo/CXX/structured-binding.cpp | 2 +-
5 files changed, 12 insertions(+), 31 deletions(-)
diff --git a/clang/test/Analysis/anonymous-decls.cpp b/clang/test/Analysis/anonymous-decls.cpp
index 76e5155b61b67..705273328e6cc 100644
--- a/clang/test/Analysis/anonymous-decls.cpp
+++ b/clang/test/Analysis/anonymous-decls.cpp
@@ -77,13 +77,11 @@ int main() {
// CHECK-NEXT: 7: [B3.6] (ImplicitCastExpr, FunctionToPointerDecay, tuple_element<0L, pair<int, int> >::type (*)(pair<int, int> &))
// CHECK-NEXT: 8: decomposition-a-b
// CHECK-NEXT: 9: [B3.7]([B3.8])
-// CHECK-NEXT: 10: [B3.9]
-// CHECK-NEXT: 11: std::tuple_element<0UL, std::pair<int, int>>::type &&a = get<0UL>(decomposition-a-b);
-// CHECK-NEXT: 12: get<1UL>
-// CHECK-NEXT: 13: [B3.12] (ImplicitCastExpr, FunctionToPointerDecay, tuple_element<1L, pair<int, int> >::type (*)(pair<int, int> &))
-// CHECK-NEXT: 14: decomposition-a-b
-// CHECK-NEXT: 15: [B3.13]([B3.14])
-// CHECK-NEXT: 16: [B3.15]
-// CHECK-NEXT: 17: std::tuple_element<1UL, std::pair<int, int>>::type &&b = get<1UL>(decomposition-a-b);
+// CHECK-NEXT: 10: std::tuple_element<0UL, std::pair<int, int>>::type a = get<0UL>(decomposition-a-b);
+// CHECK-NEXT: 11: get<1UL>
+// CHECK-NEXT: 12: [B3.11] (ImplicitCastExpr, FunctionToPointerDecay, tuple_element<1L, pair<int, int> >::type (*)(pair<int, int> &))
+// CHECK-NEXT: 13: decomposition-a-b
+// CHECK-NEXT: 14: [B3.12]([B3.13])
+// CHECK-NEXT: 15: std::tuple_element<1UL, std::pair<int, int>>::type b = get<1UL>(decomposition-a-b);
// CHECK-NEXT: Preds (1): B1
// CHECK-NEXT: Succs (1): B2
diff --git a/clang/test/Analysis/live-bindings-test.cpp b/clang/test/Analysis/live-bindings-test.cpp
index 7660e9c9904d7..eebd928145522 100644
--- a/clang/test/Analysis/live-bindings-test.cpp
+++ b/clang/test/Analysis/live-bindings-test.cpp
@@ -108,19 +108,8 @@ namespace std {
};
}
-void no_warning_on_tuple_types_copy(Mytuple t) {
- auto [a, b] = t; // no-warning
-}
-
Mytuple getMytuple();
-void deconstruct_tuple_types_warning() {
- // The initializers reference the decomposed region, so the warning is not reported
- // FIXME: ideally we want to ignore that the initializers reference the decomposed region, and report the warning,
- // though the first step towards that is to handle DeadCode if the initializer is CXXConstructExpr.
- auto [a, b] = getMytuple(); // no-warning
-}
-
int deconstruct_tuple_types_no_warning() {
auto [a, b] = getMytuple(); // no-warning
return a + b;
diff --git a/clang/test/CXX/dcl.decl/dcl.decomp/p3.cpp b/clang/test/CXX/dcl.decl/dcl.decomp/p3.cpp
index b7fef12a40b38..8a7c8561543d7 100644
--- a/clang/test/CXX/dcl.decl/dcl.decomp/p3.cpp
+++ b/clang/test/CXX/dcl.decl/dcl.decomp/p3.cpp
@@ -222,9 +222,6 @@ template<> struct std::tuple_size<constant::Q> { static const int value = 3; };
template<int N> struct std::tuple_element<N, constant::Q> { typedef int type; };
namespace constant {
Q q;
- // This creates and lifetime-extends a temporary to hold the result of each get() call.
- auto [a, b, c] = q; // expected-note {{temporary}}
- static_assert(a == 0); // expected-error {{constant expression}} expected-note {{temporary}}
constexpr bool f() {
auto [a, b, c] = q;
diff --git a/clang/test/CodeGenCXX/cxx1z-decomposition.cpp b/clang/test/CodeGenCXX/cxx1z-decomposition.cpp
index 228a6fd910248..a2f5a91aa9b2e 100644
--- a/clang/test/CodeGenCXX/cxx1z-decomposition.cpp
+++ b/clang/test/CodeGenCXX/cxx1z-decomposition.cpp
@@ -33,8 +33,7 @@ template<typename T> T &make();
// CHECK: @_ZDC2a12a2E ={{.*}} global {{.*}} zeroinitializer, align 4
auto [a1, a2] = make<A>();
// CHECK: @_ZDC2b12b2E ={{.*}} global {{.*}} zeroinitializer, align 1
-// CHECK: @b1 ={{.*}} global ptr null, align 8
-// CHECK: @_ZGR2b1_ = internal global {{.*}} zeroinitializer, align 1
+// CHECK: @b1 ={{.*}} global {{.*}} zeroinitializer, align 1
// CHECK: @b2 ={{.*}} global ptr null, align 8
// CHECK: @_ZGR2b2_ = internal global i32 0, align 4
auto [b1, b2] = make<B>();
@@ -50,9 +49,8 @@ auto [e1, e2] = make<E>();
// CHECK: @_Z4makeI1BERT_v()
// CHECK: call i32 @_Z3getILi0EEDa1B()
-// CHECK: call void @_ZN1XC1E1Y(ptr {{[^,]*}} @_ZGR2b1_, i32
-// CHECK: call i32 @__cxa_atexit({{.*}}@_ZN1XD1Ev{{.*}}@_ZGR2b1_
-// CHECK: store ptr @_ZGR2b1_,
+// CHECK: call void @_ZN1XC1E1Y(ptr {{[^,]*}} @b1, i32
+// CHECK: call i32 @__cxa_atexit({{.*}}@_ZN1XD1Ev{{.*}}@b1
//
// CHECK: call noundef double @_Z3getILi1EEDa1B()
// CHECK: fptosi double %{{.*}} to i32
@@ -149,9 +147,8 @@ int test_static_tuple() {
// CHECK: br i1
// CHECK: @__cxa_guard_acquire({{.*}} @_ZGVZ17test_static_tuplevE2x1)
// CHECK: call {{.*}} @_Z3getILi0EEDa1B(
- // CHECK: call {{.*}} @_ZN1XC1E1Y({{.*}} @_ZGRZ17test_static_tuplevE2x1_,
- // CHECK: call {{.*}} @__cxa_atexit({{.*}} @_ZN1XD1Ev, {{.*}} @_ZGRZ17test_static_tuplevE2x1_
- // CHECK: store {{.*}} @_ZGRZ17test_static_tuplevE2x1_, {{.*}} @_ZZ17test_static_tuplevE2x1
+ // CHECK: call {{.*}} @_ZN1XC1E1Y({{.*}} @_ZZ17test_static_tuplevE2x1,
+ // CHECK: call {{.*}} @__cxa_atexit({{.*}} @_ZN1XD1Ev, {{.*}} @_ZZ17test_static_tuplevE2x1
// CHECK: call void @__cxa_guard_release({{.*}} @_ZGVZ17test_static_tuplevE2x1)
// Initialization of the secret 'x2' variable.
diff --git a/clang/test/DebugInfo/CXX/structured-binding.cpp b/clang/test/DebugInfo/CXX/structured-binding.cpp
index 51818e7e16f00..e124a53762be6 100644
--- a/clang/test/DebugInfo/CXX/structured-binding.cpp
+++ b/clang/test/DebugInfo/CXX/structured-binding.cpp
@@ -17,7 +17,7 @@
// CHECK: #dbg_declare(ptr %p, ![[VAR_13:[0-9]+]], !DIExpression()
// CHECK: getelementptr inbounds nuw %struct.A, ptr {{.*}}, i32 0, i32 1, !dbg ![[Y1_DEBUG_LOC:[0-9]+]]
// CHECK: getelementptr inbounds nuw %struct.A, ptr {{.*}}, i32 0, i32 1, !dbg ![[Y2_DEBUG_LOC:[0-9]+]]
-// CHECK: load ptr, ptr %z2, {{.*}}!dbg ![[Z2_DEBUG_LOC:[0-9]+]]
+// CHECK: load i32, ptr %z2, {{.*}}!dbg ![[Z2_DEBUG_LOC:[0-9]+]]
// CHECK: getelementptr inbounds [2 x i32], ptr {{.*}}, i{{64|32}} 0, i{{64|32}} 1, !dbg ![[A2_DEBUG_LOC:[0-9]+]]
// CHECK: getelementptr inbounds nuw { i32, i32 }, ptr {{.*}}, i32 0, i32 1, !dbg ![[C2_DEBUG_LOC:[0-9]+]]
// CHECK: extractelement <2 x i32> {{.*}}, i32 1, !dbg ![[V2_DEBUG_LOC:[0-9]+]]
More information about the cfe-commits
mailing list