[clang] [Clang] Constexpr Structured Bindings : The easy parts (PR #160337)

Corentin Jabot via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 24 09:01:01 PDT 2025


https://github.com/cor3ntin updated https://github.com/llvm/llvm-project/pull/160337

>From c69bccf2d510d08b4c0b1036e4d3b05f2ee2413a Mon Sep 17 00:00:00 2001
From: Corentin Jabot <corentinjabot at gmail.com>
Date: Mon, 22 Sep 2025 19:29:44 +0200
Subject: [PATCH 1/2] [Clang] Constexpr Structured Bindings : The easy parts

This implements the easy parts of P2686R5.
Ie allowing constexpr structured vbinding of structs and arrays.

References to constexpr variables / support for tuple is left for a
future PR.

Until we implement the whole thing, the feature is not enabled
as an extension in older language modes.

Trying to use it as a tuple does produce errors but not meaningful
ones.

We could add a better diagnostic if we fail to complete the
implementation before the end of the clang 22 cycle.
---
 .../clang/Basic/DiagnosticSemaKinds.td        | 10 +--
 clang/lib/Sema/SemaDeclCXX.cpp                | 86 ++++++++-----------
 clang/test/Parser/cxx1z-decomposition.cpp     | 14 ++-
 clang/test/SemaCXX/cxx17-compat.cpp           |  6 +-
 .../cxx2c-binding-pack-nontemplate.cpp        |  4 +-
 clang/test/SemaCXX/cxx2c-decomposition.cpp    | 76 ++++++++++++++++
 6 files changed, 133 insertions(+), 63 deletions(-)
 create mode 100644 clang/test/SemaCXX/cxx2c-decomposition.cpp

diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index eef9414668809..6e04bd4ae700a 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -35,9 +35,8 @@ defm decomp_decl : CXX17Compat<"decomposition declarations are">;
 defm inline_variable : CXX17Compat<"inline variables are">;
 
 // C++20 compatibility with C++17 and earlier.
-defm decomp_decl_spec : CXX20Compat<
-  "decomposition declaration declared "
-  "%plural{1:'%1'|:with '%1' specifiers}0 is">;
+defm decomp_decl_spec
+    : CXX20Compat<"decomposition declaration declared '%0' is">;
 defm constexpr_local_var_no_init : CXX20Compat<
   "uninitialized variable in a constexpr %select{function|constructor}0 is">;
 defm constexpr_function_try_block : CXX20Compat<
@@ -593,9 +592,8 @@ def warn_modifying_shadowing_decl :
 // C++ decomposition declarations
 def err_decomp_decl_context : Error<
   "decomposition declaration not permitted in this context">;
-def err_decomp_decl_spec : Error<
-  "decomposition declaration cannot be declared "
-  "%plural{1:'%1'|:with '%1' specifiers}0">;
+def err_decomp_decl_spec
+    : Error<"decomposition declaration cannot be declared '%0'">;
 def err_decomp_decl_type : Error<
   "decomposition declaration cannot be declared with type %0; "
   "declared type must be 'auto' or reference to 'auto'">;
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index fb57b43882911..677735cf63025 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -768,58 +768,44 @@ Sema::ActOnDecompositionDeclarator(Scope *S, Declarator &D,
   // C++23 [dcl.pre]/6:
   //   Each decl-specifier in the decl-specifier-seq shall be static,
   //   thread_local, auto (9.2.9.6 [dcl.spec.auto]), or a cv-qualifier.
+  // C++23 [dcl.pre]/7:
+  //   Each decl-specifier in the decl-specifier-seq shall be constexpr,
+  //   constinit, static, thread_local, auto, or a cv-qualifier
   auto &DS = D.getDeclSpec();
-  {
-    // Note: While constrained-auto needs to be checked, we do so separately so
-    // we can emit a better diagnostic.
-    SmallVector<StringRef, 8> BadSpecifiers;
-    SmallVector<SourceLocation, 8> BadSpecifierLocs;
-    SmallVector<StringRef, 8> CPlusPlus20Specifiers;
-    SmallVector<SourceLocation, 8> CPlusPlus20SpecifierLocs;
-    if (auto SCS = DS.getStorageClassSpec()) {
-      if (SCS == DeclSpec::SCS_static) {
-        CPlusPlus20Specifiers.push_back(DeclSpec::getSpecifierName(SCS));
-        CPlusPlus20SpecifierLocs.push_back(DS.getStorageClassSpecLoc());
-      } else {
-        BadSpecifiers.push_back(DeclSpec::getSpecifierName(SCS));
-        BadSpecifierLocs.push_back(DS.getStorageClassSpecLoc());
-      }
-    }
-    if (auto TSCS = DS.getThreadStorageClassSpec()) {
-      CPlusPlus20Specifiers.push_back(DeclSpec::getSpecifierName(TSCS));
-      CPlusPlus20SpecifierLocs.push_back(DS.getThreadStorageClassSpecLoc());
-    }
-    if (DS.hasConstexprSpecifier()) {
-      BadSpecifiers.push_back(
-          DeclSpec::getSpecifierName(DS.getConstexprSpecifier()));
-      BadSpecifierLocs.push_back(DS.getConstexprSpecLoc());
-    }
-    if (DS.isInlineSpecified()) {
-      BadSpecifiers.push_back("inline");
-      BadSpecifierLocs.push_back(DS.getInlineSpecLoc());
-    }
-
-    if (!BadSpecifiers.empty()) {
-      auto &&Err = Diag(BadSpecifierLocs.front(), diag::err_decomp_decl_spec);
-      Err << (int)BadSpecifiers.size()
-          << llvm::join(BadSpecifiers.begin(), BadSpecifiers.end(), " ");
-      // Don't add FixItHints to remove the specifiers; we do still respect
-      // them when building the underlying variable.
-      for (auto Loc : BadSpecifierLocs)
-        Err << SourceRange(Loc, Loc);
-    } else if (!CPlusPlus20Specifiers.empty()) {
-      auto &&Warn = DiagCompat(CPlusPlus20SpecifierLocs.front(),
-                               diag_compat::decomp_decl_spec);
-      Warn << (int)CPlusPlus20Specifiers.size()
-           << llvm::join(CPlusPlus20Specifiers.begin(),
-                         CPlusPlus20Specifiers.end(), " ");
-      for (auto Loc : CPlusPlus20SpecifierLocs)
-        Warn << SourceRange(Loc, Loc);
-    }
-    // We can't recover from it being declared as a typedef.
-    if (DS.getStorageClassSpec() == DeclSpec::SCS_typedef)
-      return nullptr;
+  auto DiagBadSpecifier = [&](StringRef Name, SourceLocation Loc) {
+    Diag(Loc, diag::err_decomp_decl_spec) << Name;
+  };
+
+  auto DiagCpp20Specifier = [&](StringRef Name, SourceLocation Loc) {
+    DiagCompat(Loc, diag_compat::decomp_decl_spec) << Name;
+  };
+
+  if (auto SCS = DS.getStorageClassSpec()) {
+    if (SCS == DeclSpec::SCS_static)
+      DiagCpp20Specifier(DeclSpec::getSpecifierName(SCS),
+                         DS.getStorageClassSpecLoc());
+    else
+      DiagBadSpecifier(DeclSpec::getSpecifierName(SCS),
+                       DS.getStorageClassSpecLoc());
   }
+  if (auto TSCS = DS.getThreadStorageClassSpec())
+    DiagCpp20Specifier(DeclSpec::getSpecifierName(TSCS),
+                       DS.getThreadStorageClassSpecLoc());
+
+  if (DS.isInlineSpecified())
+    DiagBadSpecifier("inline", DS.getInlineSpecLoc());
+
+  if (ConstexprSpecKind ConstexprSpec = DS.getConstexprSpecifier();
+      ConstexprSpec != ConstexprSpecKind::Unspecified) {
+    if (ConstexprSpec == ConstexprSpecKind::Consteval ||
+        !getLangOpts().CPlusPlus26)
+      DiagBadSpecifier(DeclSpec::getSpecifierName(ConstexprSpec),
+                       DS.getConstexprSpecLoc());
+  }
+
+  // We can't recover from it being declared as a typedef.
+  if (DS.getStorageClassSpec() == DeclSpec::SCS_typedef)
+    return nullptr;
 
   // C++2a [dcl.struct.bind]p1:
   //   A cv that includes volatile is deprecated
diff --git a/clang/test/Parser/cxx1z-decomposition.cpp b/clang/test/Parser/cxx1z-decomposition.cpp
index b7a8d30bd16c5..274e24ea55522 100644
--- a/clang/test/Parser/cxx1z-decomposition.cpp
+++ b/clang/test/Parser/cxx1z-decomposition.cpp
@@ -83,11 +83,19 @@ namespace BadSpecifiers {
       friend auto &[g] = n; // expected-error {{'auto' not allowed}} expected-error {{friends can only be classes or functions}}
     };
     typedef auto &[h] = n; // expected-error {{cannot be declared 'typedef'}}
-    constexpr auto &[i] = n; // expected-error {{cannot be declared 'constexpr'}}
+    constexpr auto &[i] = n; // pre2c-error {{cannot be declared 'constexpr'}}
   }
 
-  static constexpr inline thread_local auto &[j1] = n; // expected-error {{cannot be declared with 'constexpr inline' specifiers}}
-  static thread_local auto &[j2] = n; // cxx17-warning {{declared with 'static thread_local' specifiers is a C++20 extension}}
+  static constexpr inline thread_local auto &[j1] = n;
+  // pre2c-error at -1 {{cannot be declared 'constexpr'}} \
+  // expected-error at -1 {{cannot be declared 'inline'}} \
+  // cxx17-warning at -1 {{declared 'static' is a C++20 extension}} \
+  // cxx17-warning at -1 {{declared 'thread_local' is a C++20 extension}}
+
+  static thread_local auto &[j2] = n;
+  // cxx17-warning at -1 {{declared 'static' is a C++20 extension}}\
+  // cxx17-warning at -1 {{declared 'thread_local' is a C++20 extension}}
+
 
   inline auto &[k] = n; // expected-error {{cannot be declared 'inline'}}
 
diff --git a/clang/test/SemaCXX/cxx17-compat.cpp b/clang/test/SemaCXX/cxx17-compat.cpp
index 81b3e1fde5493..99e41d818a6c3 100644
--- a/clang/test/SemaCXX/cxx17-compat.cpp
+++ b/clang/test/SemaCXX/cxx17-compat.cpp
@@ -83,9 +83,11 @@ static auto [cx, cy, cz] = C();
 void f() {
   static thread_local auto [cx, cy, cz] = C();
 #if __cplusplus <= 201703L
-    // expected-warning at -2 {{decomposition declaration declared with 'static thread_local' specifiers is a C++20 extension}}
+    // expected-warning at -2 {{decomposition declaration declared 'static' is a C++20 extension}}
+    // expected-warning at -3 {{decomposition declaration declared 'thread_local' is a C++20 extension}}
 #else
-    // expected-warning at -4 {{decomposition declaration declared with 'static thread_local' specifiers is incompatible with C++ standards before C++20}}
+    // expected-warning at -5 {{decomposition declaration declared 'static' is incompatible with C++ standards before C++20}}
+    // expected-warning at -6 {{decomposition declaration declared 'thread_local' is incompatible with C++ standards before C++20}}
 #endif
 }
 
diff --git a/clang/test/SemaCXX/cxx2c-binding-pack-nontemplate.cpp b/clang/test/SemaCXX/cxx2c-binding-pack-nontemplate.cpp
index a4f0bcdb4270b..638a2d805c2c5 100644
--- a/clang/test/SemaCXX/cxx2c-binding-pack-nontemplate.cpp
+++ b/clang/test/SemaCXX/cxx2c-binding-pack-nontemplate.cpp
@@ -10,8 +10,8 @@ void decompose_array() {
   auto [x, ...rest, y] = arr;
 
   // cxx26-warning at +4 {{structured binding packs are incompatible with C++ standards before C++2c}}
-  // cxx23-warning at +3 {{structured binding packs are a C++2c extension}}
-  // nontemplate-error at +2 {{decomposition declaration cannot be declared 'constexpr'}}
+  // cxx23-error at +3 {{decomposition declaration cannot be declared 'constexpr'}}
+  // cxx23-warning at +2 {{structured binding packs are a C++2c extension}}
   // nontemplate-error at +1 {{pack declaration outside of template}}
   constexpr auto [x_c, ...rest_c, y_c] = arr;
 }
diff --git a/clang/test/SemaCXX/cxx2c-decomposition.cpp b/clang/test/SemaCXX/cxx2c-decomposition.cpp
new file mode 100644
index 0000000000000..99278c6575ef1
--- /dev/null
+++ b/clang/test/SemaCXX/cxx2c-decomposition.cpp
@@ -0,0 +1,76 @@
+// RUN: %clang_cc1 -std=c++2c %s -triple x86_64-unknown-linux-gnu -verify=expected
+// RUN: %clang_cc1 -std=c++2c %s -triple x86_64-unknown-linux-gnu -verify=expected -fexperimental-new-constant-interpreter
+
+namespace std {
+  using size_t = decltype(sizeof(0));
+  template<typename> struct tuple_size;
+  template<size_t, typename> struct tuple_element;
+}
+
+struct Y { int n = 0; };
+struct X { X(); X(Y); X(const X&); ~X(); int k = 42;}; // #X-decl
+struct Z { constexpr Z(): i (43){}; int i;}; // #Z-decl
+struct Z2 { constexpr Z2(): i (0){}; int i; ~Z2();}; // #Z2-decl
+
+struct Bit { constexpr Bit(): i(1), j(1){}; int i: 2; int j:2;};
+
+struct A { int a : 13; bool b; };
+
+struct B {};
+template<> struct std::tuple_size<B> { enum { value = 2 }; };
+template<> struct std::tuple_size<const B> { enum { value = 2 }; };
+template<> struct std::tuple_element<0, const B> { using type = Y; };
+template<> struct std::tuple_element<1, const B> { using type = const int&; };
+template<int N>
+constexpr auto get(B) {
+  if constexpr (N == 0)
+    return Y();
+  else
+    return 0.0;
+}
+
+
+constexpr auto [t1] = Y {42};
+static_assert(t1 == 42);
+
+constexpr int i[] = {1, 2};
+constexpr auto [t2, t3] = i;
+static_assert(t2 == 1);
+static_assert(t3 == 2);
+
+constexpr auto [t4] = X();
+// expected-error at -1 {{constexpr variable cannot have non-literal type 'const X'}} \
+// expected-note@#X-decl {{'X' is not literal because it is not an aggregate and has no constexpr constructors other than copy or move constructors}}
+
+constexpr auto [t5] = Z();
+static_assert(t5 == 43);
+
+constexpr auto [t6] = Z2();
+//expected-error at -1 {{constexpr variable cannot have non-literal type 'const Z2'}}
+// expected-note@#Z2-decl {{'Z2' is not literal because its destructor is not constexpr}}
+
+constexpr auto [t7, t8] = Bit();
+static_assert(t7 == 1);
+static_assert(t8 == 1);
+
+void test_tpl(auto) {
+    constexpr auto [...p] = Bit();
+    static_assert(((p == 1) && ...));
+}
+
+void test() {
+    test_tpl(0);
+}
+
+// FIXME : support tuple
+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}}
+
+constinit auto [init1] = Y {42};
+constinit auto [init2] = X {};  // expected-error {{variable does not have a constant initializer}} \
+// expected-note {{required by 'constinit' specifier here}} \
+// expected-note {{non-constexpr constructor 'X' cannot be used in a constant expression}} \
+// expected-note@#X-decl {{declared here}}

>From d3357f7c5b0a6daa2b579c281e6e7d7a22bc1133 Mon Sep 17 00:00:00 2001
From: Corentin Jabot <corentinjabot at gmail.com>
Date: Wed, 24 Sep 2025 18:00:45 +0200
Subject: [PATCH 2/2] changelog

---
 clang/docs/ReleaseNotes.rst | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index c898784b3f93e..6330a7298af11 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -141,6 +141,10 @@ C++ Language Changes
 C++2c Feature Support
 ^^^^^^^^^^^^^^^^^^^^^
 
+- Started the implementation of `P2686R5 <https://wg21.link/P2686R5>`_ Constexpr structured bindings.
+  At this timem, references to constexpr and decomposition of _tuple-like_ types are not supported
+  (only arrays and aggregates are).
+
 C++23 Feature Support
 ^^^^^^^^^^^^^^^^^^^^^
 



More information about the cfe-commits mailing list