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