[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