[PATCH] c++, libstdc++, v2: Implement C++26 P2641R4 - Checking if a union alternative is active

Jason Merrill jason@redhat.com
Thu Aug 27 18:51:05 GMT 2026


On 8/27/26 9:31 AM, Jakub Jelinek wrote:
> On Wed, Aug 26, 2026 at 04:06:21PM -0700, Jason Merrill wrote:
>> On 8/26/26 4:08 AM, Jakub Jelinek wrote:
>>> The following patch attempts to implement this paper (though for now just
>>> using what the FE prooovides instead of introducing new stuff, see below).
>>
>> prooovides
> 
> Sorry, my keyboard is dying, will replace it during the weekend.
> 
>>> There are a few differences/problems, some of those I'd like to address
>>> incrementally:
>>> 1) unlike in clang++, the builtin itself is not consteval, just usable
>>>      in constant expressions; people shouldn't be using the builtin directly
>>>      and when it is called from consteval std::is_within_lifetime (or any
>>>      other consteval function or consteval block), it is guaranteed to be
>>>      immediately evaluated.  The builtin if not immediately
>>>      evaluated just quickly checks for some errors (number of arguments and
>>>      that the argument is a pointer) and optimizes itself out.  Having
>>>      a consteval builtin would be a novel thing and I'm not convinced it
>>>      is necessary.
>>
>> Hmm, silently returning false at runtime seems like a lie.
> 
> Ok, changed the patch so that __builtin_is_within_lifetime is a consteval
> function and adjusted testcase accordingly (for uses from within
> std::is_within_lifetime obviously it changed nothing, but for direct calls
> in some spots yes).
> 
>>> 3) we don't implement std::allocator<T>::allocate correctly at constant
>>>      evaluation time, in particular
>>>      https://eel.is/c++draft/memory#allocator.members-5.sentence-2
>>>      That is pretty much the same thing as in P3726R2 std::start_lifetime
>>>      does, therefore my current plan is once this is in, to restart working
>>>      on P3726R2 and add a new CONSTRUCTOR bit next to CONSTRUCTOR_NO_CLEARING
>>>      which would mean the left out elements are not in lifetime even when the
>>>      whole CONSTRUCTOR is, add support for that in
>>>      cxx_eval_is_within_lifetime and implicitly pretend the builtin is
>>>      used in std::allocator<T>::allocate; this is covered in the testsuite
>>>      in within-lifetime3.C but is guarded with #if 0
>>
>> Hmm, is it possible to have an array CONSTRUCTOR with
>> CONSTRUCTOR_NO_CLEARING which doesn't represent that situation?  If the
>> array is out of lifetime it shouldn't have a CONSTRUCTOR.  There isn't the
>> same period of construction issue that there is with classes.
> 
> Consider
> consteval bool
> foo ()
> {
>    int a[4];
>    if (!__builtin_is_within_lifetime (&a[2]))
>      return false;
>    a[1] = 42;
>    if (!__builtin_is_within_lifetime (&a[2]))
>      return false;
>    return true;
> }
> 
> static_assert (foo ());
> 
> Already on the first call to the builtin a has *valp of CONSTRUCTOR
> with the ARRAY_TYPE with no elements and CONSTRUCTOR_NO_CLEARING, and it
> means the whole array including all elements is within lifetime, rather than
> that the array is and no children are.  This is now also in
> within-lifetime1.C.
> So I don't see how we can avoid a separate bit for it.

Makes sense.

>>> 4) the standard says in https://eel.is/c++draft/type.traits#meta.const.eval-5
>>>      that "p points to an object that is usable in constant expressions
>>>      or whose complete object's lifetime began within E".
>>>      The patch handles const vars outside of current evaluation (and in that
>>>      case diagnoses/makes non-constant accesses to their mutable members if
>>>      any since those subobjects aren't usable in constant expressions), but
>>>      also diagnoses/making non-constant if the complete object is gone
>>>      (use after scope, use after deallocation, that all seems to me like
>>>      clear UB that should not be constexpr but am not sure that the current
>>>      standard wording is clear about that).
>>
>> Yes, I think that's UB->non-constant because the pointer no longer "points
>> to an object" after the storage is gone.
> 
> Thinking about this more (but nothing in the patch yet), I think we have a
> problem that void_node for complete objects can mean two different things.
> It can mean we've deallocated storage for it, or it can mean we've called
> std::destroy_at.  The former should result in an error if not quiet and
> be *non_constant_p, while the latter should just mean the builtin returns
> false.  Now, if it is just a subobject, void_node can mean just one thing,
> that it is not in lifetime but still has storage allocated.
> Say:
> 
> consteval bool
> foo (bool x)
> {
>    int *p;
>    {
>      int a;
>      p = &a;
>      if (!__builtin_is_within_lifetime (&a))
>        return false;
>      std::destroy_at (&a);
>      if (__builtin_is_within_lifetime (&a))
>        return false;
>      std::construct_at (&a);
>      if (!__builtin_is_within_lifetime (&a))
>        return false;
>    }
>    if (x)
>      __builtin_is_within_lifetime (p);
>    return true;
> }
> 
> static_assert (foo (false));
> bool a = foo (true);
> 
> This fails incorrectly IMHO with the current patch, already the
> second builtin call results in it being non-constant.
> 
> Shall we remove the hash_map record on deallocation instead or store
> something magic other than void_node?

Removing it sounds elegant, if there isn't a diagnostic quality reason 
to go the other way.

>> As you've seen, I'm arguing in CWG that this specification should be changed
>> to "started initialization".
> 
> I've adjusted the testsuite assuming that is changed (so removed the xfails
> for it).  On the other side I've added the testcase I've posted in the
> patch description rather than in the patch and xfailed that.
> 
>>> +/* Perform __builtin_is_within_lifetime call checking, return false
>>> +   when errors are reported.  */
>>> +
>>> +bool
>>> +check_builtin_is_within_lifetime (location_t loc, int nargs, tree *args,
>>> +				  tsubst_flags_t complain)
>>
>> These checks seem like they belong at semantic analysis time, rather than
>> later during evaluation or gimplification?
> 
> Ok, moved to finish_call_expr instead (for normal builtins, such checks are
> usually either inn resolve_overloaded_builtin or in say builtins.cc folding
> of the builtin, but resolve_overloaded_builtin isn't called for FE
> builtins).
> 
>>> +		  "__builtin_within_lifetime");
>>
>> Missing "is" in a bunch of places.
> 
> Fixed.
> 
>>> +	      error_at (loc, "%qs on allocated storage after deallocation "
>>> +			     "is not a constant expression",
>>
>> This won't survive re-indentation without added parens.
> 
> Fixed.
> 
>>> +  /* In the loop below, val contains the value of the current
>>> +     container (usually a CONSTRUCTOR), or NULL_TREE if we are in zero
>>> +     initialized container (i.e. first union member is in lifetime),
>>> +     or void_list_node if we are in uninitialized container
>>> +     (non-union is still within lifetime, none of the union members are
>>> +     within lifetime.  */
>>> +  if (val == NULL_TREE)
>>> +    val = void_list_node;
>>
>> Hmm, it seems fragile to use NULL_TREE as a magic value below when it
>> generally means already means something different (i.e. vacuous
>> initialization), which is why you replace it here.
> 
> So do you want instead 2 magic values, one for zero initialization
> implicit stuff and one for uninitialized implicit stuff?
> Or it can be a separate flag or flags from the val variable.
> Say bool is_zero_init = false, is_unitialized = false;

Either sounds fine, but I lean toward 2 magic values.

>>> +  FOR_EACH_VEC_ELT_REVERSE (refs, i, ref)
>>> +    switch (TREE_CODE (ref))
>>> +      {
>>> +      case REALPART_EXPR:
>>> +      case IMAGPART_EXPR:
>>> +	if (val
>>> +	    && TREE_CODE (val) == COMPLEX_EXPR
>>> +	    && (TREE_OPERAND (val, TREE_CODE (ref) == IMAGPART_EXPR)
>>> +		== void_node))
>>> +	  return boolean_false_node;
>>> +	return boolean_true_node;
>>
>> So this will return true if val is null or void_node or other
>> non-COMPLEX_EXPR?  That seems wrong.
> 
> If val is NULL, then it is the zero initialization case and both
> real and imag parts are within lifetime.
> If val is void_list_node, then it is the uninitialized case and
> again both are within lifetime.
> If val is void_node, then this shouldn't have been reached, we should
> have returned boolean_false_node already in previous iteration (if any)
> or if toplevel, right now reject as non-constant (see above for why
> that isn't correct).
> I think val perhaps could be in some cases CONSTRUCTOR with
> CONSTRUCTOR_NO_PADDING (for uninitialized value of the whole _Complex),
> but there are no FIELD_DECLs/indexes for _Complex so I think otherwise
> it should be only COMPLEX_CST or COMPLEX_EXPR (and COMPLEX_EXPR with
> void_node first and/or second argument stands clearly (as seen in the
> testcase) for destroy_at part).
> 
>>> +	if (val == NULL_TREE || val == void_list_node)
>>> +	  continue;
>>
>> So if val is void_list_node and there are no unions involved, we'll iterate
>> through the remaining refs and then return true below?
>>
>> It's not clear to me why it's ever useful to set val to void_list_node over
>> immediately returning false; if a containing object is uninitialized, all
>> subobjects are as well.
> 
> They are uninitialized, but within lifetime (at least if they have vacuous
> initialization).  So, I think we want to return true, but need to iterate
> over the remaining refs just in case there is a union member access (then
> we don't want to return true but false).

Ah, yes.

>>> +	if (TREE_CODE (val) != CONSTRUCTOR)
>>> +	  return boolean_false_node;
>>
>> This seems like an assertion rather than a return false?
> 
> Wonder about error_mark_node or something similar, but perhaps I could make
> it into assert, yes (not done in the following patch yet).
> 

> --- a/gcc/cp/constexpr.cc	2026-08-27 13:46:12.726137380 +0200
> +++ b/gcc/cp/constexpr.cc	2026-08-27 14:43:57.050845241 +0200
> @@ -1253,6 +1253,13 @@ public:
>   	return *p;
>       return NULL_TREE;
>     }
> +  tree *get_value_ptr (tree t)
> +  {
> +    if (tree *p = values.get (t))
> +      if (*p != void_node)
> +	return p;
> +    return nullptr;
> +  }
>     tree *get_value_ptr (tree t, bool initializing)
>     {
>       if (modifiable && !modifiable->contains (t))

It seems a bit confusing to have overloads with subtly different 
meanings (modifying or not).  We might call the new one 
get_raw_value_ptr and not have special handling of void_node, so it's 
simpler to distinguish between out-of-lifetime and no object?

> +	  else if (DECL_P (obj) && ctx->global->is_outside_lifetime (obj))
> +	    {
> +	      error_at (loc, "%qs on %qE outside its lifetime is not a "
> +			"constant expression",
> +			"__builtin_is_within_lifetime", obj);
> +	      inform (DECL_SOURCE_LOCATION (obj), "declared here");
> +	    }

I'd expect this to return false once we resolve the distinction between 
end of lifetime and end of storage (no object).

Jason



More information about the Libstdc++ mailing list