On Fri, Sep 4, 2026 at 10:44 PM Jason Merrill <[email protected]> wrote:

> 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  <[email protected]>
> >
> >       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?
>
Because A is struct, so a.a[1] is consitutient value of a. and is erronous,
so I would
expect that error. Zero-initialization of global variables seems irrelevant
here, as a is
copy-initialized from such value

> Jason
>
>

Reply via email to