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?

Jason

Reply via email to