Skip to content

Conversation

cor3ntin
Copy link
Contributor

This implements the easy parts of P2686R5.
Ie allowing constexpr structured binding 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.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Sep 23, 2025
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.
@llvmbot
Copy link
Member

llvmbot commented Sep 23, 2025

@llvm/pr-subscribers-clang

Author: Corentin Jabot (cor3ntin)

Changes

This implements the easy parts of P2686R5.
Ie allowing constexpr structured binding 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.


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

6 Files Affected:

  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+4-6)
  • (modified) clang/lib/Sema/SemaDeclCXX.cpp (+37-50)
  • (modified) clang/test/Parser/cxx1z-decomposition.cpp (+11-3)
  • (modified) clang/test/SemaCXX/cxx17-compat.cpp (+4-2)
  • (modified) clang/test/SemaCXX/cxx2c-binding-pack-nontemplate.cpp (+2-2)
  • (added) clang/test/SemaCXX/cxx2c-decomposition.cpp (+76)
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..86ba474e064a3 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -29,6 +29,7 @@
 #include "clang/AST/TypeOrdering.h"
 #include "clang/Basic/AttributeCommonInfo.h"
 #include "clang/Basic/PartialDiagnostic.h"
+#include "clang/Basic/SourceLocation.h"
 #include "clang/Basic/Specifiers.h"
 #include "clang/Basic/TargetInfo.h"
 #include "clang/Lex/LiteralSupport.h"
@@ -768,58 +769,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@-1 {{cannot be declared 'constexpr'}} \
+  // expected-error@-1 {{cannot be declared 'inline'}} \
+  // cxx17-warning@-1 {{declared 'static' is a C++20 extension}} \
+  // cxx17-warning@-1 {{declared 'thread_local' is a C++20 extension}}
+
+  static thread_local auto &[j2] = n;
+  // cxx17-warning@-1 {{declared 'static' is a C++20 extension}}\
+  // cxx17-warning@-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@-2 {{decomposition declaration declared with 'static thread_local' specifiers is a C++20 extension}}
+    // expected-warning@-2 {{decomposition declaration declared 'static' is a C++20 extension}}
+    // expected-warning@-3 {{decomposition declaration declared 'thread_local' is a C++20 extension}}
 #else
-    // expected-warning@-4 {{decomposition declaration declared with 'static thread_local' specifiers is incompatible with C++ standards before C++20}}
+    // expected-warning@-5 {{decomposition declaration declared 'static' is incompatible with C++ standards before C++20}}
+    // expected-warning@-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@+4 {{structured binding packs are incompatible with C++ standards before C++2c}}
-  // cxx23-warning@+3 {{structured binding packs are a C++2c extension}}
-  // nontemplate-error@+2 {{decomposition declaration cannot be declared 'constexpr'}}
+  // cxx23-error@+3 {{decomposition declaration cannot be declared 'constexpr'}}
+  // cxx23-warning@+2 {{structured binding packs are a C++2c extension}}
   // nontemplate-error@+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@-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@-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@-1 {{static assertion expression is not an integral constant expression}} \
+// expected-note@-1 {{read of temporary is not allowed in a constant expression outside the expression that created the temporary}}\
+// expected-note@-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}}

@cor3ntin cor3ntin force-pushed the corentin/structured_bindings_pt1 branch from 5a785ca to c69bccf Compare September 23, 2025 16:20
Copy link
Collaborator

@erichkeane erichkeane left a comment

Choose a reason for hiding this comment

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

Perhaps a release note here? Though one that I'd expect we edit along the way.

@cor3ntin cor3ntin merged commit 83331cc into llvm:main Sep 24, 2025
10 checks passed
@cor3ntin cor3ntin deleted the corentin/structured_bindings_pt1 branch September 24, 2025 21:39
^^^^^^^^^^^^^^^^^^^^^

- 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
Copy link

Choose a reason for hiding this comment

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

typo: timem

mahesh-attarde pushed a commit to mahesh-attarde/llvm-project that referenced this pull request Oct 3, 2025
This implements the easy parts of P2686R5.
Ie allowing constexpr structured binding 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.
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants