[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