The following patch adds CWG 2867 support for namespace locals.
Those vars are just pushed into {static,tls}_aggregates chain, then
pruned from those lists, separated by priority and finally emitted into
the corresponding dynamic initialization functions.
The patch adds two flags used on the TREE_LIST nodes in those lists,
one marks the structured binding base variable and/or associated ref
extended temps, another marks the vars initialized using get methods.
The flags are preserved across the pruning, for splitting into by priority
all associated decls of a structured binding using tuple* are forced
into the same priority as the first one, and finally when actually emitting
code, CLEANUP_POINT_EXPRs are disabled in the base initializer(s) and
code from the bases and non-bases together is wrapped into a single
CLEANUP_POINT_EXPR.
2025-01-27 Jakub Jelinek <jakub@redhat.com>
PR c++/115769
gcc/cp/
* cp-tree.h (STATIC_INIT_DECOMP_BASE_P): Define.
(STATIC_INIT_DECOMP_NONBASE_P): Define.
* decl.cc (cp_finish_decl): Mark nodes in {static,tls}_aggregates
emitted for namespace scope structured bindings with
STATIC_INIT_DECOMP_{,NON}BASE_P flags when needed.
* decl2.cc (decomp_handle_one_var, decomp_finalize_var_list): New
functions.
(emit_partial_init_fini_fn): Use them.
(prune_vars_needing_no_initialization): Assert
STATIC_INIT_DECOMP_*BASE_P is not set on DECL_EXTERNAL vars to be
pruned out.
(partition_vars_for_init_fini): Use same priority for
consecutive STATIC_INIT_DECOMP_*BASE_P vars and propagate
those flags to new TREE_LISTs when possible. Formatting fix.
(handle_tls_init): Use decomp_handle_one_var and
decomp_finalize_var_list functions.
gcc/testsuite/
* g++.dg/DRs/dr2867-5.C: New test.
* g++.dg/DRs/dr2867-6.C: New test.
* g++.dg/DRs/dr2867-7.C: New test.
* g++.dg/DRs/dr2867-8.C: New test.
BASELINK_FUNCTIONS_MAYBE_INCOMPLETE_P (in BASELINK)
BIND_EXPR_VEC_DTOR (in BIND_EXPR)
ATOMIC_CONSTR_EXPR_FROM_CONCEPT_P (in ATOMIC_CONSTR)
+ STATIC_INIT_DECOMP_BASE_P (in the TREE_LIST for {static,tls}_aggregates)
2: IDENTIFIER_KIND_BIT_2 (in IDENTIFIER_NODE)
ICS_THIS_FLAG (in _CONV)
DECL_INITIALIZED_BY_CONSTANT_EXPRESSION_P (in VAR_DECL)
IMPLICIT_CONV_EXPR_BRACED_INIT (in IMPLICIT_CONV_EXPR)
PACK_EXPANSION_AUTO_P (in *_PACK_EXPANSION)
contract_semantic (in ASSERTION_, PRECONDITION_, POSTCONDITION_STMT)
+ STATIC_INIT_DECOMP_NONBASE_P (in the TREE_LIST
+ for {static,tls}_aggregates)
3: IMPLICIT_RVALUE_P (in NON_LVALUE_EXPR or STATIC_CAST_EXPR)
ICS_BAD_FLAG (in _CONV)
FN_TRY_BLOCK_P (in TRY_BLOCK)
extern bool flag_noexcept_type;
+/* True if this TREE_LIST in {static,tls}_aggregates is a for dynamic
+ initialization of namespace scope structured binding base or related
+ extended ref init temps. Temporaries from the initialization of
+ STATIC_INIT_DECOMP_BASE_P dynamic initializers should be destroyed only
+ after the last STATIC_INIT_DECOMP_NONBASE_P dynamic initializer following
+ it. */
+#define STATIC_INIT_DECOMP_BASE_P(NODE) \
+ TREE_LANG_FLAG_1 (TREE_LIST_CHECK (NODE))
+
+/* True if this TREE_LIST in {static,tls}_aggregates is a for dynamic
+ initialization of namespace scope structured binding non-base
+ variable using get. */
+#define STATIC_INIT_DECOMP_NONBASE_P(NODE) \
+ TREE_LANG_FLAG_2 (TREE_LIST_CHECK (NODE))
+
/* A list of namespace-scope objects which have constructors or
destructors which reside in the global scope. The decl is stored
in the TREE_VALUE slot and the initializer is stored in the
bool var_definition_p = false;
tree auto_node;
auto_vec<tree> extra_cleanups;
+ tree aggregates1 = NULL_TREE;
struct decomp_cleanup {
tree decl;
cp_decomp *&decomp;
}
if (decomp)
- cp_maybe_mangle_decomp (decl, decomp);
+ {
+ cp_maybe_mangle_decomp (decl, decomp);
+ if (TREE_STATIC (decl) && !DECL_FUNCTION_SCOPE_P (decl))
+ {
+ if (CP_DECL_THREAD_LOCAL_P (decl))
+ aggregates1 = tls_aggregates;
+ else
+ aggregates1 = static_aggregates;
+ }
+ }
/* If this is a local variable that will need a mangled name,
register it now. We must do this before processing the
if (decomp_init)
add_stmt (decomp_init);
+ if (decomp
+ && var_definition_p
+ && TREE_STATIC (decl)
+ && !DECL_FUNCTION_SCOPE_P (decl))
+ {
+ tree &aggregates3 = (CP_DECL_THREAD_LOCAL_P (decl)
+ ? tls_aggregates : static_aggregates);
+ tree aggregates2 = aggregates3;
+ if (aggregates2 != aggregates1)
+ {
+ cp_finish_decomp (decl, decomp);
+ decomp = NULL;
+ if (aggregates3 != aggregates2)
+ {
+ /* If there are dynamic initializers for the structured
+ binding base or associated extended ref temps and also
+ dynamic initializers for the structured binding non-base
+ vars, mark them. */
+ for (tree t = aggregates3; t != aggregates2; t = TREE_CHAIN (t))
+ STATIC_INIT_DECOMP_NONBASE_P (t) = 1;
+ for (tree t = aggregates2; t != aggregates1; t = TREE_CHAIN (t))
+ STATIC_INIT_DECOMP_BASE_P (t) = 1;
+ }
+ }
+ }
+
if (was_readonly)
TREE_READONLY (decl) = 1;
DECL_STATIC_FUNCTION_P (current_function_decl) = 0;
}
+/* Helper function for emit_partial_init_fini_fn and handle_tls_init.
+ For structured bindings, disable stmts_are_full_exprs_p ()
+ on STATIC_INIT_DECOMP_BASE_P nodes, reenable it on the
+ first STATIC_INIT_DECOMP_NONBASE_P node and emit all the
+ STATIC_INIT_DECOMP_BASE_P and STATIC_INIT_DECOMP_NONBASE_P
+ consecutive nodes in a single STATEMENT_LIST wrapped with
+ CLEANUP_POINT_EXPR. */
+
+static inline tree
+decomp_handle_one_var (tree node, tree sl, bool *saw_nonbase,
+ int save_stmts_are_full_exprs_p)
+{
+ if (sl && !*saw_nonbase && STATIC_INIT_DECOMP_NONBASE_P (node))
+ {
+ *saw_nonbase = true;
+ current_stmt_tree ()->stmts_are_full_exprs_p
+ = save_stmts_are_full_exprs_p;
+ }
+ else if (sl && *saw_nonbase && !STATIC_INIT_DECOMP_NONBASE_P (node))
+ {
+ sl = pop_stmt_list (sl);
+ sl = maybe_cleanup_point_expr_void (sl);
+ add_stmt (sl);
+ sl = NULL_TREE;
+ }
+ if (sl == NULL_TREE && STATIC_INIT_DECOMP_BASE_P (node))
+ {
+ sl = push_stmt_list ();
+ *saw_nonbase = false;
+ current_stmt_tree ()->stmts_are_full_exprs_p = 0;
+ }
+ return sl;
+}
+
+/* Similarly helper called when the whole var list is processed. */
+
+static inline void
+decomp_finalize_var_list (tree sl, int save_stmts_are_full_exprs_p)
+{
+ if (sl)
+ {
+ current_stmt_tree ()->stmts_are_full_exprs_p
+ = save_stmts_are_full_exprs_p;
+ sl = pop_stmt_list (sl);
+ sl = maybe_cleanup_point_expr_void (sl);
+ add_stmt (sl);
+ }
+}
+
/* Generate code to do the initialization or destruction of the decls in VARS,
a TREE_LIST of VAR_DECL with static storage duration.
Whether initialization or destruction is performed is specified by INITP. */
finish_if_stmt_cond (target_dev_p, nonhost_if_stmt);
}
+ tree sl = NULL_TREE;
+ int save_stmts_are_full_exprs_p = stmts_are_full_exprs_p ();
+ bool saw_nonbase = false;
for (tree node = vars; node; node = TREE_CHAIN (node))
{
tree decl = TREE_VALUE (node);
tree init = TREE_PURPOSE (node);
- /* We will emit 'init' twice, and it is modified in-place during
- gimplification. Make a copy here. */
+ sl = decomp_handle_one_var (node, sl, &saw_nonbase,
+ save_stmts_are_full_exprs_p);
+ /* We will emit 'init' twice, and it is modified in-place during
+ gimplification. Make a copy here. */
if (omp_target)
{
/* We've already emitted INIT in the host version of the ctor/dtor
/* Do one initialization or destruction. */
one_static_initialization_or_destruction (initp, decl, init);
}
+ decomp_finalize_var_list (sl, save_stmts_are_full_exprs_p);
if (omp_target)
{
here. */
if (DECL_EXTERNAL (decl))
{
+ gcc_checking_assert (!STATIC_INIT_DECOMP_BASE_P (t)
+ && !STATIC_INIT_DECOMP_NONBASE_P (t));
var = &TREE_CHAIN (t);
continue;
}
void
partition_vars_for_init_fini (tree var_list, priority_map_t *(&parts)[4])
{
+ unsigned priority = 0;
+ enum { none, base, nonbase } decomp_state = none;
for (auto node = var_list; node; node = TREE_CHAIN (node))
{
tree decl = TREE_VALUE (node);
tree init = TREE_PURPOSE (node);
bool has_cleanup = !TYPE_HAS_TRIVIAL_DESTRUCTOR (TREE_TYPE (decl));
- unsigned priority = DECL_EFFECTIVE_INIT_PRIORITY (decl);
+ if (decomp_state == base && STATIC_INIT_DECOMP_NONBASE_P (node))
+ decomp_state = nonbase;
+ else if (decomp_state == nonbase && !STATIC_INIT_DECOMP_NONBASE_P (node))
+ decomp_state = none;
+ if (decomp_state == none)
+ priority = DECL_EFFECTIVE_INIT_PRIORITY (decl);
if (init || (flag_use_cxa_atexit && has_cleanup))
{
parts[true] = priority_map_t::create_ggc ();
auto &slot = parts[true]->get_or_insert (priority);
slot = tree_cons (init, decl, slot);
+ if (init
+ && STATIC_INIT_DECOMP_BASE_P (node)
+ && decomp_state == none)
+ {
+ /* If one or more STATIC_INIT_DECOMP_BASE_P with at least
+ one init is followed by at least one
+ STATIC_INIT_DECOMP_NONBASE_P with init, mark it in the
+ resulting chain as well. */
+ for (tree n = TREE_CHAIN (node); n; n = TREE_CHAIN (n))
+ if (STATIC_INIT_DECOMP_BASE_P (n))
+ continue;
+ else if (STATIC_INIT_DECOMP_NONBASE_P (n))
+ {
+ if (TREE_PURPOSE (n))
+ {
+ decomp_state = base;
+ break;
+ }
+ else
+ continue;
+ }
+ else
+ break;
+ }
+ if (init && decomp_state == base)
+ STATIC_INIT_DECOMP_BASE_P (slot) = 1;
+ else if (decomp_state == nonbase)
+ STATIC_INIT_DECOMP_NONBASE_P (slot) = 1;
}
if (!flag_use_cxa_atexit && has_cleanup)
}
if (flag_openmp
- && lookup_attribute ("omp declare target", DECL_ATTRIBUTES (decl)))
+ && lookup_attribute ("omp declare target", DECL_ATTRIBUTES (decl)))
{
priority_map_t **omp_parts = parts + 2;
omp_parts[true] = priority_map_t::create_ggc ();
auto &slot = omp_parts[true]->get_or_insert (priority);
slot = tree_cons (init, decl, slot);
+ if (init && decomp_state == base)
+ STATIC_INIT_DECOMP_BASE_P (slot) = 1;
+ else if (decomp_state == nonbase)
+ STATIC_INIT_DECOMP_NONBASE_P (slot) = 1;
}
if (!flag_use_cxa_atexit && has_cleanup)
finish_expr_stmt (cp_build_modify_expr (loc, guard, NOP_EXPR,
boolean_true_node,
tf_warning_or_error));
+ tree sl = NULL_TREE;
+ int save_stmts_are_full_exprs_p = stmts_are_full_exprs_p ();
+ bool saw_nonbase = false;
for (; vars; vars = TREE_CHAIN (vars))
{
tree var = TREE_VALUE (vars);
tree init = TREE_PURPOSE (vars);
+ sl = decomp_handle_one_var (vars, sl, &saw_nonbase,
+ save_stmts_are_full_exprs_p);
one_static_initialization_or_destruction (/*initp=*/true, var, init);
/* Output init aliases even with -fno-extern-tls-init. */
gcc_assert (alias != NULL);
}
}
+ decomp_finalize_var_list (sl, save_stmts_are_full_exprs_p);
finish_then_clause (if_stmt);
finish_if_stmt (if_stmt);
--- /dev/null
+// CWG2867 - Order of initialization for structured bindings.
+// { dg-do run { target c++11 } }
+// { dg-options "" }
+
+#define assert(X) do { if (!(X)) __builtin_abort(); } while (0)
+
+namespace std {
+ template<typename T> struct tuple_size;
+ template<int, typename> struct tuple_element;
+}
+
+int a, c, d, i;
+
+struct A {
+ A () { assert (c == 3); ++c; }
+ ~A () { ++a; }
+ template <int I> int &get () const { assert (c == 5 + I); ++c; return i; }
+};
+
+template <> struct std::tuple_size <A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, A> { using type = int; };
+template <> struct std::tuple_size <const A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, const A> { using type = int; };
+
+struct B {
+ B () { assert (c >= 1 && c <= 2); ++c; }
+ ~B () { assert (c >= 9 && c <= 10); ++c; }
+};
+
+struct C {
+ constexpr C () {}
+ constexpr C (const C &) {}
+ template <int I> int &get () const { assert (d == 1 + I); ++d; return i; }
+};
+
+template <> struct std::tuple_size <C> { static const int value = 3; };
+template <int I> struct std::tuple_element <I, C> { using type = int; };
+template <> struct std::tuple_size <const C> { static const int value = 3; };
+template <int I> struct std::tuple_element <I, const C> { using type = int; };
+
+A
+foo (const B &, const B &)
+{
+ A a;
+ assert (c == 4);
+ ++c;
+ return a;
+}
+
+constexpr C
+foo (const C &, const C &)
+{
+ return C {};
+}
+
+int
+bar (int &x, int y)
+{
+ x = y;
+ return y;
+}
+
+int
+baz (int &x, int y)
+{
+ assert (x == y);
+ return y;
+}
+
+struct E {
+ ~E () { assert (a == 2); }
+};
+
+E e;
+int c1 = bar (c, 1);
+const auto &[x, y, z, w] = foo (B {}, B {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+int c2 = baz (c, 11);
+int d1 = bar (d, 1);
+const auto &[s, t, u] = foo (C {}, C {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+int d2 = baz (d, 4);
+int c3 = bar (c, 1);
+auto [x2, y2, z2, w2] = foo (B {}, B {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+int c4 = baz (c, 11);
+int d3 = bar (d, 1);
+auto [s2, t2, u2] = foo (C {}, C {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+int d4 = baz (d, 4);
+
+int
+main ()
+{
+ assert (a == 0);
+}
--- /dev/null
+// CWG2867 - Order of initialization for structured bindings.
+// { dg-do run { target c++11 } }
+// { dg-options "" }
+
+#define assert(X) do { if (!(X)) __builtin_abort(); } while (0)
+
+namespace std {
+ template<typename T> struct tuple_size;
+ template<int, typename> struct tuple_element;
+}
+
+int a, c;
+
+struct C {
+ C () { assert (c >= 5 && c <= 17 && (c - 5) % 4 == 0); ++c; }
+ ~C () { assert (c >= 8 && c <= 20 && c % 4 == 0); ++c; }
+};
+
+struct D {
+ D () { assert (c >= 7 && c <= 19 && (c - 7) % 4 == 0); ++c; }
+ ~D () { assert (a % 5 != 4); ++a; }
+};
+
+struct A {
+ A () { assert (c == 3); ++c; }
+ ~A () { assert (a % 5 == 4); ++a; }
+ template <int I> D get (const C & = C{}) const { assert (c == 6 + 4 * I); ++c; return D {}; }
+};
+
+template <> struct std::tuple_size <A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, A> { using type = D; };
+template <> struct std::tuple_size <const A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, const A> { using type = D; };
+
+struct B {
+ B () { assert (c >= 1 && c <= 2); ++c; }
+ ~B () { assert (c >= 21 && c <= 22); ++c; }
+};
+
+A
+foo (const B &, const B &)
+{
+ A a;
+ assert (c == 4);
+ ++c;
+ return a;
+}
+
+int
+bar (int &x, int y)
+{
+ x = y;
+ return y;
+}
+
+int
+baz (int &x, int y)
+{
+ assert (x == y);
+ return y;
+}
+
+struct E {
+ ~E () { assert (a == 5); }
+};
+
+E e;
+int c1 = bar (c, 1);
+// First B::B () is invoked twice, then foo called, which invokes A::A ().
+// e is reference bound to the A::A () constructed temporary.
+// Then 4 times (in increasing I):
+// C::C () is invoked, get is called, D::D () is invoked, C::~C () is
+// invoked.
+// After that B::~B () is invoked twice.
+// At exit time D::~D () is invoked 4 times, then A::~A ().
+const auto &[x, y, z, w] = foo (B {}, B {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+int c2 = baz (c, 23);
+
+int
+main ()
+{
+ assert (a == 0);
+}
--- /dev/null
+// CWG2867 - Order of initialization for structured bindings.
+// { dg-do run { target c++11 } }
+// { dg-options "" }
+// { dg-add-options tls }
+// { dg-require-effective-target tls_runtime }
+
+#define assert(X) do { if (!(X)) __builtin_abort(); } while (0)
+
+namespace std {
+ template<typename T> struct tuple_size;
+ template<int, typename> struct tuple_element;
+}
+
+int a, c, d, i;
+
+struct A {
+ A () { assert (c == 3); ++c; }
+ ~A () { ++a; }
+ template <int I> int &get () const { assert (c == 5 + I); ++c; return i; }
+};
+
+template <> struct std::tuple_size <A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, A> { using type = int; };
+template <> struct std::tuple_size <const A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, const A> { using type = int; };
+
+struct B {
+ B () { assert (c >= 1 && c <= 2); ++c; }
+ ~B () { assert (c >= 9 && c <= 10); ++c; }
+};
+
+struct C {
+ constexpr C () {}
+ constexpr C (const C &) {}
+ template <int I> int &get () const { assert (d == 1 + I); ++d; return i; }
+};
+
+template <> struct std::tuple_size <C> { static const int value = 3; };
+template <int I> struct std::tuple_element <I, C> { using type = int; };
+template <> struct std::tuple_size <const C> { static const int value = 3; };
+template <int I> struct std::tuple_element <I, const C> { using type = int; };
+
+A
+foo (const B &, const B &)
+{
+ A a;
+ assert (c == 4);
+ ++c;
+ return a;
+}
+
+constexpr C
+foo (const C &, const C &)
+{
+ return C {};
+}
+
+int
+bar (int &x, int y)
+{
+ x = y;
+ return y;
+}
+
+int
+baz (int &x, int y)
+{
+ assert (x == y);
+ return y;
+}
+
+struct E {
+ ~E () { assert (a == 2); }
+};
+
+thread_local E e;
+thread_local int c1 = bar (c, 1);
+thread_local const auto &[x, y, z, w] = foo (B {}, B {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+thread_local int c2 = baz (c, 11); // { dg-warning "structured binding declaration can be 'thread_local' only in" "" { target c++17_down } .-1 }
+thread_local int d1 = bar (d, 1);
+thread_local const auto &[s, t, u] = foo (C {}, C {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+thread_local int d2 = baz (d, 4); // { dg-warning "structured binding declaration can be 'thread_local' only in" "" { target c++17_down } .-1 }
+thread_local int c3 = bar (c, 1);
+thread_local auto [x2, y2, z2, w2] = foo (B {}, B {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+thread_local int c4 = baz (c, 11); // { dg-warning "structured binding declaration can be 'thread_local' only in" "" { target c++17_down } .-1 }
+thread_local int d3 = bar (d, 1);
+thread_local auto [s2, t2, u2] = foo (C {}, C {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+thread_local int d4 = baz (d, 4); // { dg-warning "structured binding declaration can be 'thread_local' only in" "" { target c++17_down } .-1 }
+
+int
+main ()
+{
+ volatile int u = c1 + x + y + z + w + c2;
+ u += d1 + s + t + u + d2;
+ u += c3 + x2 + y2 + z2 + w2 + c4;
+ u += d3 + s2 + t2 + u2 + d4;
+ assert (a == 0);
+}
--- /dev/null
+// CWG2867 - Order of initialization for structured bindings.
+// { dg-do run { target c++11 } }
+// { dg-options "" }
+// { dg-add-options tls }
+// { dg-require-effective-target tls_runtime }
+
+#define assert(X) do { if (!(X)) __builtin_abort(); } while (0)
+
+namespace std {
+ template<typename T> struct tuple_size;
+ template<int, typename> struct tuple_element;
+}
+
+int a, c;
+
+struct C {
+ C () { assert (c >= 5 && c <= 17 && (c - 5) % 4 == 0); ++c; }
+ ~C () { assert (c >= 8 && c <= 20 && c % 4 == 0); ++c; }
+};
+
+struct D {
+ D () { assert (c >= 7 && c <= 19 && (c - 7) % 4 == 0); ++c; }
+ ~D () { assert (a % 5 != 4); ++a; }
+};
+
+struct A {
+ A () { assert (c == 3); ++c; }
+ ~A () { assert (a % 5 == 4); ++a; }
+ template <int I> D get (const C & = C{}) const { assert (c == 6 + 4 * I); ++c; return D {}; }
+};
+
+template <> struct std::tuple_size <A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, A> { using type = D; };
+template <> struct std::tuple_size <const A> { static const int value = 4; };
+template <int I> struct std::tuple_element <I, const A> { using type = D; };
+
+struct B {
+ B () { assert (c >= 1 && c <= 2); ++c; }
+ ~B () { assert (c >= 21 && c <= 22); ++c; }
+};
+
+A
+foo (const B &, const B &)
+{
+ A a;
+ assert (c == 4);
+ ++c;
+ return a;
+}
+
+int
+bar (int &x, int y)
+{
+ x = y;
+ return y;
+}
+
+int
+baz (int &x, int y)
+{
+ assert (x == y);
+ return y;
+}
+
+struct E {
+ ~E () { assert (a == 5); }
+};
+
+thread_local E e;
+thread_local int c1 = bar (c, 1);
+// First B::B () is invoked twice, then foo called, which invokes A::A ().
+// e is reference bound to the A::A () constructed temporary.
+// Then 4 times (in increasing I):
+// C::C () is invoked, get is called, D::D () is invoked, C::~C () is
+// invoked.
+// After that B::~B () is invoked twice.
+// At exit time D::~D () is invoked 4 times, then A::~A ().
+thread_local const auto &[x, y, z, w] = foo (B {}, B {}); // { dg-warning "structured bindings only available with" "" { target c++14_down } }
+thread_local int c2 = baz (c, 23); // { dg-warning "structured binding declaration can be 'thread_local' only in" "" { target c++17_down } .-1 }
+
+int
+main ()
+{
+ volatile int u = c1 + c2;
+ assert (a == 0);
+}