[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