On 8/26/26 4:08 AM, Jakub Jelinek wrote:
Hi!
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
There is a new builtin which returns true if what the argument points to
is within lifetime, false if it is not and results in non-constant
expression if it points to something non-accessible in constant expression,
or the complete object is not in lifetime. Most of the tests work
identically in clang++ which implements this paper for some time and g++
with this patch.
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.
2) this patch implements just P2641R4, not the subsequent P3450R1 paper.
The plan is to use
template<class _Up = void, typename _Tp>
consteval bool
is_within_lifetime(const _Tp* __p) noexcept
{ return __builtin_is_within_lifetime (__p)
&& __builtin_constant_p (static_cast <const volatile _Up *> (__p)
&& true); }
afterwards but we need
https://gcc.gnu.org/pipermail/gcc-patches/2026-August/thread.html#726464
finished for that; I'll restart work on that soon
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.
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.
5) for complete objects created during E evaluation, the patch uses
a new get_value_ptr method (the 2 argument one is too much store
specific, but I can't just use get_value, because I need to differentiate
between values hash map doesn't have an entry for this var yet, it
clearly has not started lifetime yet, and values hash map has a NULL_TREE
entry for that, that e.g. for scalar means it has started lifetime but
is uninitialized) and heap_deleted_identifier (for use after delete)
and is_outside_lifetime (which is use after destruction). The patch
doesn't track yet though whether the complete subobject actually began
its lifetime vs. just started being constructed, so there are 3 xfails
in within-lifetime3.C test which clang++ handles right; for
"complete object's lifetime began" I think we need some bit somewhere
set when the evaluation of constructor for the complete object finishes
(but is unclear whether we should clear it again when we start destructor
though because we don't have consteval destructors yet I'm not sure this
is actually observable). Also if we can only track it for complete
objects and not also subobjects. In any case, the problem of not
implementing this yet means that std::is_within_lifetime uses in ctors
on the currently constructed subobjects (where complete object is
not in lifetime yet) won't be non-constant but will return something
(false/true). I'd like to address this incrementally, but am not yet
sure where to record it without slowing stuff significantly down and
causing extra compile time memory use
As you've seen, I'm arguing in CWG that this specification should be
changed to "started initialization".
+/* 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?
+ "__builtin_within_lifetime");
Missing "is" in a bunch of places.
+ error_at (loc, "%qs on allocated storage after deallocation "
+ "is not a constant expression",
This won't survive re-indentation without added parens.
+ "__builtin_within_lifetime");
+ inform (DECL_SOURCE_LOCATION (obj), "allocated here");
+ }
+ 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_within_lifetime", obj);
+ inform (DECL_SOURCE_LOCATION (obj), "declared here");
+ }
+ else
+ error_at (loc, "%qs on %qE from outside current evaluation "
+ "is not a constant expression",
+ "__builtin_within_lifetime", obj);
+ }
+ *non_constant_p = true;
+ return t;
+ }
+ tree val = *valp;
+ unsigned i;
+ tree ref;
+ /* 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.
+ 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.
+ case COMPONENT_REF:
+ if (check_mutable && DECL_MUTABLE_P (TREE_OPERAND (ref, 1)))
+ {
+ if (!ctx->quiet)
+ error_at (loc, "%qs on %<mutable%> sub-object %qD",
+ "__builtin_within_lifetime", TREE_OPERAND (ref, 1));
+ *non_constant_p = true;
+ return t;
+ }
+ if (TREE_CODE (TREE_TYPE (TREE_OPERAND (ref, 0))) == UNION_TYPE)
+ {
+ tree union_type = TREE_TYPE (TREE_OPERAND (ref, 0));
+ if (val == NULL_TREE)
+ {
+ if (TREE_OPERAND (ref, 1)
+ != next_aggregate_field (TYPE_FIELDS (union_type)))
+ return boolean_false_node;
+ continue;
+ }
+ else
+ {
+ if (TREE_CODE (val) != CONSTRUCTOR)
+ return boolean_false_node;
+ if (CONSTRUCTOR_NELTS (val) == 0)
+ {
+ if (CONSTRUCTOR_NO_CLEARING (val))
+ return boolean_false_node;
+ tree first
+ = next_aggregate_field (TYPE_FIELDS (union_type));
+ if (first != TREE_OPERAND (ref, 1))
+ return boolean_false_node;
+ val = NULL_TREE;
+ continue;
+ }
+ else
+ {
+ if (CONSTRUCTOR_ELT (val, 0)->index
+ != TREE_OPERAND (ref, 1))
+ return boolean_false_node;
+ val = CONSTRUCTOR_ELT (val, 0)->value;
+ if (val == void_node)
+ return boolean_false_node;
+ continue;
+ }
+ }
+ }
+ 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.
+ if (TREE_CODE (val) != CONSTRUCTOR)
+ return boolean_false_node;
This seems like an assertion rather than a return false?
Jason