From c69bccf2d510d08b4c0b1036e4d3b05f2ee2413a Mon Sep 17 00:00:00 2001 From: Corentin Jabot Date: Mon, 22 Sep 2025 19:29:44 +0200 Subject: [PATCH 1/3] [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 BadSpecifiers; - SmallVector BadSpecifierLocs; - SmallVector CPlusPlus20Specifiers; - SmallVector 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 struct tuple_size; + template 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 { enum { value = 2 }; }; +template<> struct std::tuple_size { 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 +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}} From d3357f7c5b0a6daa2b579c281e6e7d7a22bc1133 Mon Sep 17 00:00:00 2001 From: Corentin Jabot Date: Wed, 24 Sep 2025 18:00:45 +0200 Subject: [PATCH 2/3] 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 `_ 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 ^^^^^^^^^^^^^^^^^^^^^ From 12f08fa17e1ba97331434acffab653a6b0398dba Mon Sep 17 00:00:00 2001 From: Corentin Jabot Date: Wed, 24 Sep 2025 18:25:06 +0200 Subject: [PATCH 3/3] changelog fix --- clang/docs/ReleaseNotes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 6330a7298af11..47199bc4cce92 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -142,7 +142,7 @@ C++2c Feature Support ^^^^^^^^^^^^^^^^^^^^^ - Started the implementation of `P2686R5 `_ Constexpr structured bindings. - At this timem, references to constexpr and decomposition of _tuple-like_ types are not supported + At this timem, references to constexpr and decomposition of *tuple-like* types are not supported (only arrays and aggregates are). C++23 Feature Support