[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