[PATCH] c++, libstdc++: Implement C++26 P3726R2 - Adjustments to Union Lifetime Rules [PR125845]
Jason Merrill
jason@redhat.com
Fri Sep 4 20:44:22 GMT 2026
On 9/4/26 9:57 AM, Jakub Jelinek wrote:
> Hi!
>
> Here is an updated version of the P3726R2 paper implementation, originally
> posted in May together with the other trivial union paper and one CWG:
> https://gcc.gnu.org/pipermail/gcc-patches/2026-May/thread.html#717349
> That other paper and CWG has been committed in the meantime, this one
> was deferred so that more accurate lifetime tracking is implemented (which
> was partially done in the recently committed std::is_within_lifetime patch).
>
> The major change from the earlier patch is that a new CONSTRUCTOR flag
> CONSTRUCTOR_OMITTED_NOT_WITHIN_LIFETIME_P is added, which represents
> both the C++20 std::allocator<T>::allocate required behavior as well as
> P3726R2 std::start_lifetime, where the CONSTRUCTOR itself is considered
> to be within lifetime, but none of its subobjects.
> This flag is then tested in reduced_constant_expression_p to determine
> which union elemental subobjects are actually inactive and only those are
> allowed to be omitted, and in the recently added
> __builtin_is_within_lifetime builtin constant evaluation.
>
> So far lightly tested, with:
> GXX_TESTSUITE_STDS=98,11,14,17,20,23,26,29 make check-g++ RUNTESTFLAGS="dg.exp='within-lifetime* trivial-union* feat-cxx2* constexpr-life*'"
> ok for trunk if it passes full bootstrap/regtest?
>
> 2026-09-04 Jakub Jelinek <jakub@redhat.com>
>
> P3726R2
> PR c++/125845
> gcc/c-family/
> * c-cppbuiltin.cc (c_cpp_builtins): For C++26 predefine
> __cpp_trivial_union to 202603L.
> gcc/cp/
> * cp-tree.h: Implement C++26 P3726R2 - Adjustments to Union Lifetime
> Rules.
> (CONSTRUCTOR_OMITTED_NOT_WITHIN_LIFETIME_P): Define.
> (enum cp_built_in_function): Add CP_BUILT_IN_START_LIFETIME.
> (reduced_constant_expression_p): Add a bool argument defaulted to
> false.
> * tree.cc (builtin_valid_in_constant_expr_p): Handle
> CP_BUILT_IN_START_LIFETIME.
> * decl.cc (cxx_init_decl_processing): Create __builtin_start_lifetime
> decl.
> * cp-gimplify.cc (cp_gimplify_expr): Handle
> CP_BUILT_IN_START_LIFETIME.
> * semantics.cc (finish_call_expr): Likewise.
Let's move all the builtin handling out to another function at this point.
> * constexpr.cc (cxx_eval_is_within_lifetime): For subobjects of
> CONSTRUCTOR_OMITTED_NOT_WITHIN_LIFETIME_P return boolean_false_node.
> (cxx_eval_start_lifetime): New function.
> (cxx_eval_builtin_function_call): Handle CP_BUILT_IN_START_LIFETIME.
> (reduced_constant_expression_p): Add union_elemental_subobject
> argument and propagate it to recursive calls. If set, ignore holes
> in array initializers which aren't within lifetime.
> (cxx_eval_store_expression): Add start_lifetime argument. Don't
> preevaluate init in tha tcase, return void_node early if *valp
that case
> is already non-NULL/void_node and within lifetime.
> (cxx_eval_constant_expression): For std::allocator<T>::allocate
> start lifetime of the created array but not of its subobjects.
> gcc/testsuite/
> * g++.dg/cpp26/feat-cxx26.C: Adjust __cpp_trivial_union checking.
> * g++.dg/cpp29/feat-cxx29.C: Adjust __cpp_trivial_union checking.
> * g++.dg/cpp26/trivial-union3.C: New test.
> * g++.dg/cpp26/trivial-union4.C: New test.
> * g++.dg/cpp26/trivial-union5.C: New test.
> * g++.dg/cpp26/trivial-union6.C: New test.
> * g++.dg/cpp26/within-lifetime3.C: Remove #if 0 and #endif.
> * g++.dg/cpp29/feat-cxx29.C: Adjust __cpp_trivial_union checking.
Duplicate entry.
> libstdc++-v3/
> * include/bits/version.def (start_lifetime): New.
> * include/bits/version.h: Regenerate.
> * include/bits/stl_construct.h (std::start_lifetime): New function
> template.
> * include/std/memory: Define __glibcxx_want_start_lifetime before
> including bits/version.h.
> * src/c++23/std.cc.in: Add export std::start_lifetime.
>
> --- a/gcc/cp/cp-tree.h 2026-09-03 09:45:43.310945741 +0200
> +++ b/gcc/cp/cp-tree.h 2026-09-04 14:11:50.412816957 +0200
> @@ -5098,6 +5098,10 @@ get_vec_init_expr (tree t)
> #define CONSTRUCTOR_BRACES_ELIDED_P(NODE) \
> (CONSTRUCTOR_CHECK (NODE)->base.protected_flag)
>
> +/* True if omitted fields are considered to be not within lifetime. */
> +#define CONSTRUCTOR_OMITTED_NOT_WITHIN_LIFETIME_P(NODE) \
> + (CONSTRUCTOR_CHECK (NODE)->base.deprecated_flag)
Let's document the use of these bitfields in the "Usage of
language-independent fields in a language-dependent manner" section at
the top of the file.
> --- a/gcc/cp/decl.cc 2026-09-03 09:45:43.308945768 +0200
> +++ b/gcc/cp/decl.cc 2026-09-04 13:54:14.077036599 +0200
> @@ -5679,6 +5679,13 @@ cxx_init_decl_processing (void)
> set_call_expr_flags (decl, ECF_NOTHROW | ECF_LEAF);
> SET_DECL_IMMEDIATE_FUNCTION_P (decl);
>
> + tree void_vaftype = build_varargs_function_type_list (boolean_type_node,
"void_vaftype" with bool return type seems contradictory.
> --- a/gcc/cp/semantics.cc 2026-09-03 09:45:43.312945713 +0200
> +++ b/gcc/cp/semantics.cc 2026-09-04 14:10:43.423660866 +0200
> @@ -3541,30 +3541,74 @@ finish_call_expr (tree fn, vec<tree, va_
> }
>
> if (TREE_CODE (fn) == FUNCTION_DECL
> + && fndecl_built_in_p (fn, BUILT_IN_FRONTEND))
> + switch (DECL_UNCHECKED_FUNCTION_CODE (fn))
> + {
> + tree arg, ptype, type;
> + case CP_BUILT_IN_IS_WITHIN_LIFETIME:
> + /* Unless users call the builtin directly, the following 2 checks
> + should be ensured from std::is_within_lifetime template. */
> + if (vec_safe_length (*args) != 1)
> + {
> + if (complain & tf_error)
> + error ("%qs needs a single argument",
> + "__builtin_is_within_lifetime");
> + return error_mark_node;
> + }
> + arg = (**args)[0];
> + if (error_operand_p (arg))
> return error_mark_node;
> + ptype = TREE_TYPE (arg);
> + if (!POINTER_TYPE_P (ptype))
> + {
> + if (complain & tf_error)
> + error ("%qs argument type %qT is not pointer type",
> + "__builtin_is_within_lifetime", ptype);
> + return error_mark_node;
> + }
> + break;
> + case CP_BUILT_IN_START_LIFETIME:
> + /* Unless users call the builtin directly, the following 2 checks
> + should be ensured from std::start_lifetime template. */
> + if (vec_safe_length (*args) != 1)
> + {
> + if (complain & tf_error)
> + error ("%qs needs a single argument",
> + "__builtin_start_lifetime");
> + return error_mark_node;
> + }
> + arg = (**args)[0];
> + if (error_operand_p (arg))
> return error_mark_node;
> + ptype = TREE_TYPE (arg);
> + if (!POINTER_TYPE_P (ptype))
> + {
> + if (complain & tf_error)
> + error ("%qs argument type %qT is not pointer type",
> + "__builtin_start_lifetime", ptype);
> + return error_mark_node;
> + }
> + type = TREE_TYPE (ptype);
> + if (!complete_type_or_else (type, NULL_TREE))
> + return error_mark_node;
> + if (!CP_AGGREGATE_TYPE_P (type))
> + {
> + if (complain & tf_error)
> + error ("%qs argument type %qT is not a pointer to "
> + "aggregate type", "__builtin_start_lifetime",
> + ptype);
> + return error_mark_node;
> + }
> + if (!implicit_lifetime_type_p (type))
> + {
> + if (complain & tf_error)
> + error ("%qs argument type %qT is not a pointer to "
> + "implicit-lifetime type",
> + "__builtin_start_lifetime", ptype);
> + return error_mark_node;
> + }
> + break;
We probably want a default: case, to avoid warnings if nothing else.
> + }
>
> /* A call to a namespace-scope function. */
> result = build_new_function_call (fn, args, orig_complain);
> --- a/gcc/cp/constexpr.cc 2026-09-03 09:43:50.766477808 +0200
> +++ b/gcc/cp/constexpr.cc 2026-09-04 14:32:59.700779280 +0200
> @@ -5107,10 +5154,13 @@ cxx_eval_call_expression (const constexp
> initializes all the members, the CONSTRUCTOR_NO_CLEARING flag will be
> cleared. If called recursively on a FIELD_DECL's CONSTRUCTOR, SZ
> is DECL_SIZE of the FIELD_DECL, otherwise NULL.
> + UNION_ELEMENTAL_SUBOBJECT is true when recursing on C++ >= 26 union
> + elemental subobjects.
> FIXME speed this up, it's taking 16% of compile time on sieve testcase. */
>
> bool
> -reduced_constant_expression_p (tree t, tree sz /* = NULL_TREE */)
> +reduced_constant_expression_p (tree t, tree sz /* = NULL_TREE */,
> + bool union_elemental_subobject /* = false */)
> {
> if (t == NULL_TREE)
> return false;
> @@ -5134,7 +5184,11 @@ reduced_constant_expression_p (tree t, t
> return false;
> if (CONSTRUCTOR_NO_CLEARING (t))
> {
> - if (TREE_CODE (TREE_TYPE (t)) == ARRAY_TYPE)
> + if (union_elemental_subobject
> + && TREE_CODE (TREE_TYPE (t)) == ARRAY_TYPE
> + && CONSTRUCTOR_OMITTED_NOT_WITHIN_LIFETIME_P (t))
> + field = NULL_TREE;
> + else if (TREE_CODE (TREE_TYPE (t)) == ARRAY_TYPE)
> {
> /* There must be a valid constant initializer at every array
> index. */
> @@ -5168,12 +5222,25 @@ reduced_constant_expression_p (tree t, t
> active member has no constituent values, so all constituent
> values are constant. */
> field = NULL_TREE;
> + union_elemental_subobject = cxx_dialect >= cxx26;
I'd prefer to compute union_elemental_subobject regardless of
cxx_dialect, and only check cxx_dialect when applying it above.
> }
> else
> - field = next_subobject_field (TYPE_FIELDS (TREE_TYPE (t)));
> + {
> + field = next_subobject_field (TYPE_FIELDS (TREE_TYPE (t)));
> + union_elemental_subobject = false;
> + }
> }
> else
> - field = NULL_TREE;
> + {
> + field = NULL_TREE;
> + if (cxx_dialect >= cxx26)
> + {
> + if (TREE_CODE (TREE_TYPE (t)) == UNION_TYPE)
> + union_elemental_subobject = true;
> + else if (TREE_CODE (TREE_TYPE (t)) != ARRAY_TYPE)
> + union_elemental_subobject = false;
> + }
> + }
> for (auto &e: CONSTRUCTOR_ELTS (t))
> {
> /* If VAL is null, we're in the middle of initializing this
> @@ -5183,7 +5250,8 @@ reduced_constant_expression_p (tree t, t
> && (TREE_CODE (e.index)
> == FIELD_DECL))
> ? DECL_SIZE (e.index)
> - : NULL_TREE))
> + : NULL_TREE,
> + union_elemental_subobject))
> return false;
> /* We want to remove initializers for empty fields in a struct to
> avoid confusing output_constructor. */
> @@ -8213,13 +8281,14 @@ modifying_const_object_p (tree_code code
> return false;
> }
>
> -/* Evaluate an INIT_EXPR or MODIFY_EXPR. */
> +/* Evaluate an INIT_EXPR or MODIFY_EXPR. START_LIFETIME is true
> + when evaluating __builtin_start_lifetime. */
>
> static tree
> cxx_eval_store_expression (const constexpr_ctx *ctx, tree t,
> value_cat lval,
> bool *non_constant_p, bool *overflow_p,
> - tree *jump_target)
> + tree *jump_target, bool start_lifetime/*=false*/)
Hmm, I don't love adding a parameter for this case, but I guess a
clobber won't work because this is a nop for an object that's already in
lifetime.
> {
> constexpr_ctx new_ctx = *ctx;
>
> @@ -8232,7 +8301,7 @@ cxx_eval_store_expression (const constex
>
> tree type = TREE_TYPE (target);
> bool preeval = SCALAR_TYPE_P (type) || TREE_CODE (t) == MODIFY_EXPR;
> - if (preeval && !TREE_CLOBBER_P (init))
> + if (preeval && !TREE_CLOBBER_P (init) && !start_lifetime)
> {
> /* Ignore var = .DEFERRED_INIT (); for now, until PR121965 is fixed. */
> if (flag_auto_var_init > AUTO_INIT_UNINITIALIZED
> @@ -8573,12 +8642,13 @@ cxx_eval_store_expression (const constex
> index);
> *non_constant_p = true;
> }
> - else if (!is_access_expr
> - || (TREE_CLOBBER_P (init)
> - && CLOBBER_KIND (init) >= CLOBBER_OBJECT_END)
> - || (TREE_CODE (t) == MODIFY_EXPR
> - && CLASS_TYPE_P (inner)
> - && !type_has_non_deleted_trivial_default_ctor (inner)))
> + else if ((!is_access_expr
> + || (TREE_CLOBBER_P (init)
> + && CLOBBER_KIND (init) >= CLOBBER_OBJECT_END)
> + || (TREE_CODE (t) == MODIFY_EXPR
> + && CLASS_TYPE_P (inner)
> + && !type_has_non_deleted_trivial_default_ctor (inner)))
> + && !start_lifetime)
> {
> /* Diagnose changing active union member after initialization
> without a valid member access expression, as described in
> @@ -8675,6 +8745,11 @@ cxx_eval_store_expression (const constex
> empty_base = true;
> }
>
> + /* For __builtin_start_lifetime, if it is already within lifetime,
> + don't do anything. */
> + if (start_lifetime && *valp != NULL_TREE && *valp != void_node)
> + return void_node;
> +
> /* Detect modifying a constant object in constexpr evaluation.
> We have found a const object that is being modified. Figure out
> if we need to issue an error. Consider
> @@ -8780,7 +8855,7 @@ cxx_eval_store_expression (const constex
> || *valp == void_node
> || (same_type_ignoring_top_level_qualifiers_p
> (TREE_TYPE (*valp), type)));
> - if (empty_base)
> + if (empty_base && !start_lifetime)
I'm wondering about the empty_base && start_lifetime case. empty_base
means valp is pointing to the initializer for a containing class at this
point. But if that object had a value we would have returned early
above -- and not done anything to start the subobject lifetime. So this
change only affects the case where the enclosing class is uninitialized.
With this change, we end up at *valp = init below, setting the *valp
for the enclosing class to a value of the wrong type.
So I don't think this change is right. Which test hits this? I don't
see any empty classes in the new tests.
> {
> /* Just evaluate the initializer and return, since there's no actual data
> to store, and we didn't build a CONSTRUCTOR. */
> @@ -10730,6 +10805,17 @@ cxx_eval_constant_expression (const cons
> TREE_TYPE (var) = new_type;
> TREE_TYPE (TREE_OPERAND (op, 0))
> = build_pointer_type (TREE_TYPE (var));
> + if (is_std_allocator_allocate (ctx->call)
> + && id_equal (DECL_NAME (ctx->call->fundef->decl), "allocate"))
It's surprising that a function named is_std_allocator_allocate also
returns true for "deallocate".
> --- a/gcc/testsuite/g++.dg/cpp26/trivial-union6.C 2026-09-04 13:54:14.081215755 +0200
> +++ b/gcc/testsuite/g++.dg/cpp26/trivial-union6.C 2026-09-04 13:54:14.081215755 +0200
> @@ -0,0 +1,70 @@
> +// P3074R7 - trivial unions (was std::uninitialized<T>)
> +// P3726R2 - Adjustments to Union Lifetime Rules
> +// { dg-do compile { target c++20 } }
> +
> +struct A { int a[2]; };
> +union B { int a; int b[2]; };
> +union C { int a; int b[2][2]; };
> +struct D { B a; };
> +struct E { C a; };
> +union F { int a; A b; };
> +union G { int a; A b[2]; };
> +
> +constexpr A fA () { A a; a.a[0] = 1; return a; }
> +constexpr A a = fA (); // { dg-error "is not a constant expression" "" { xfail *-*-* } }
I don't see any discussion of this xfail above (or a comment here). Is
there a PR?
Jason
More information about the Libstdc++
mailing list